tiger-agent 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,217 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ // ─── Zhipu AI (BigModel) JWT auth ──────────────────────────────────────────
6
+ // Their v4 API requires HS256 JWT derived from the api-key (format: "id.secret")
7
+ function b64url(buf) {
8
+ return buf.toString('base64').replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
9
+ }
10
+
11
+ function zhipuJwt(apiKey) {
12
+ const dot = apiKey.indexOf('.');
13
+ if (dot === -1) return apiKey; // fallback: treat as plain token
14
+ const id = apiKey.slice(0, dot);
15
+ const secret = apiKey.slice(dot + 1);
16
+ const now = Math.floor(Date.now() / 1000);
17
+ const hdr = b64url(Buffer.from(JSON.stringify({ alg: 'HS256', sign_type: 'SIGN' })));
18
+ const pay = b64url(Buffer.from(JSON.stringify({ api_key: id, exp: now + 3600, timestamp: now })));
19
+ const sig = b64url(crypto.createHmac('sha256', secret).update(`${hdr}.${pay}`).digest());
20
+ return `${hdr}.${pay}.${sig}`;
21
+ }
22
+
23
+ // ─── OpenAI-compatible adapters ────────────────────────────────────────────
24
+
25
+ function standardFormat(messages, options) {
26
+ const payload = {
27
+ model: options.model,
28
+ messages,
29
+ temperature: options.temperature ?? 0.3
30
+ };
31
+ if (options.tools && options.tools.length) payload.tools = options.tools;
32
+ if (options.tool_choice) payload.tool_choice = options.tool_choice;
33
+ return payload;
34
+ }
35
+
36
+ function standardParse(data) {
37
+ const message = data.choices?.[0]?.message || {};
38
+ const u = data.usage || {};
39
+ const tokens = (u.prompt_tokens || 0) + (u.completion_tokens || 0);
40
+ return { message, tokens };
41
+ }
42
+
43
+ // ─── Claude (Anthropic) adapters ───────────────────────────────────────────
44
+
45
+ function claudeFormat(messages, options) {
46
+ const systemMsg = messages.find((m) => m.role === 'system');
47
+ const rest = messages.filter((m) => m.role !== 'system');
48
+
49
+ // Convert tool definitions: OpenAI → Claude
50
+ let tools;
51
+ if (options.tools && options.tools.length) {
52
+ tools = options.tools.map((t) => ({
53
+ name: t.function.name,
54
+ description: t.function.description || '',
55
+ input_schema: t.function.parameters || { type: 'object', properties: {} }
56
+ }));
57
+ }
58
+
59
+ // Convert message content: tool_calls & tool results
60
+ const converted = rest.map((m) => {
61
+ if (m.role === 'assistant' && Array.isArray(m.tool_calls) && m.tool_calls.length) {
62
+ const content = [];
63
+ if (m.content) content.push({ type: 'text', text: m.content });
64
+ for (const tc of m.tool_calls) {
65
+ let input = {};
66
+ try { input = JSON.parse(tc.function.arguments || '{}'); } catch (_) {}
67
+ content.push({ type: 'tool_use', id: tc.id, name: tc.function.name, input });
68
+ }
69
+ return { role: 'assistant', content };
70
+ }
71
+ if (m.role === 'tool') {
72
+ return {
73
+ role: 'user',
74
+ content: [{ type: 'tool_result', tool_use_id: m.tool_call_id, content: String(m.content || '') }]
75
+ };
76
+ }
77
+ return m;
78
+ });
79
+
80
+ const payload = {
81
+ model: options.model,
82
+ max_tokens: options.max_tokens || 8192,
83
+ messages: converted
84
+ };
85
+ if (systemMsg) payload.system = systemMsg.content;
86
+ if (tools && tools.length) {
87
+ payload.tools = tools;
88
+ // Convert OpenAI tool_choice format → Claude format
89
+ const tc = options.tool_choice;
90
+ if (tc && tc !== 'none') {
91
+ if (tc === 'auto' || tc === 'required') {
92
+ payload.tool_choice = { type: tc === 'required' ? 'any' : 'auto' };
93
+ } else if (tc && typeof tc === 'object' && tc.type === 'function') {
94
+ payload.tool_choice = { type: 'tool', name: tc.function.name };
95
+ }
96
+ }
97
+ }
98
+ return payload;
99
+ }
100
+
101
+ function claudeParse(data) {
102
+ const content = Array.isArray(data.content) ? data.content : [];
103
+ const textBlock = content.find((b) => b.type === 'text');
104
+ const toolUseBlocks = content.filter((b) => b.type === 'tool_use');
105
+
106
+ const message = { role: 'assistant', content: textBlock ? textBlock.text : '' };
107
+ if (toolUseBlocks.length) {
108
+ message.tool_calls = toolUseBlocks.map((tb) => ({
109
+ id: tb.id,
110
+ type: 'function',
111
+ function: { name: tb.name, arguments: JSON.stringify(tb.input || {}) }
112
+ }));
113
+ }
114
+
115
+ const u = data.usage || {};
116
+ const tokens = (u.input_tokens || 0) + (u.output_tokens || 0);
117
+ return { message, tokens };
118
+ }
119
+
120
+ // ─── Provider registry ─────────────────────────────────────────────────────
121
+
122
+ function buildProviders(env) {
123
+ return {
124
+ kimi: {
125
+ id: 'kimi',
126
+ name: 'Kimi Code',
127
+ baseUrl: (env.KIMI_BASE_URL || 'https://api.kimi.com/coding/v1').replace(/\/$/, ''),
128
+ chatModel: env.KIMI_CHAT_MODEL ? env.KIMI_CHAT_MODEL.replace(/^kimi-coding\//, '') : 'k2p5',
129
+ embedModel: env.KIMI_EMBED_MODEL || '',
130
+ apiKey: env.KIMI_CODE_API_KEY || env.KIMI_API_KEY || '',
131
+ userAgent: env.KIMI_USER_AGENT || 'KimiCLI/0.77',
132
+ authHeaders: (key) => ({ Authorization: `Bearer ${key}` }),
133
+ chatPath: '/chat/completions',
134
+ embedPath: '/embeddings',
135
+ formatRequest: standardFormat,
136
+ parseResponse: standardParse,
137
+ timeout: Number(env.KIMI_TIMEOUT_MS || 30000)
138
+ },
139
+
140
+ moonshot: {
141
+ id: 'moonshot',
142
+ name: 'Kimi Moonshot',
143
+ baseUrl: (env.MOONSHOT_BASE_URL || 'https://api.moonshot.cn/v1').replace(/\/$/, ''),
144
+ chatModel: env.MOONSHOT_MODEL || 'kimi-k1',
145
+ embedModel: env.MOONSHOT_EMBED_MODEL || 'kimi-embedding-v1',
146
+ apiKey: env.MOONSHOT_API_KEY || env.KIMI_API_KEY || '',
147
+ userAgent: '',
148
+ authHeaders: (key) => ({ Authorization: `Bearer ${key}` }),
149
+ chatPath: '/chat/completions',
150
+ embedPath: '/embeddings',
151
+ formatRequest: standardFormat,
152
+ parseResponse: standardParse,
153
+ timeout: Number(env.KIMI_TIMEOUT_MS || 30000)
154
+ },
155
+
156
+ zai: {
157
+ id: 'zai',
158
+ name: 'Z.ai (Zhipu)',
159
+ baseUrl: (env.ZAI_BASE_URL || 'https://open.bigmodel.cn/api/paas/v4').replace(/\/$/, ''),
160
+ chatModel: env.ZAI_MODEL || 'glm-5',
161
+ embedModel: env.ZAI_EMBED_MODEL || '',
162
+ apiKey: env.ZAI_API_KEY || '',
163
+ userAgent: '',
164
+ authHeaders: (key) => ({ Authorization: `Bearer ${zhipuJwt(key)}` }),
165
+ chatPath: '/chat/completions',
166
+ embedPath: '/embeddings',
167
+ formatRequest: standardFormat,
168
+ parseResponse: standardParse,
169
+ timeout: Number(env.ZAI_TIMEOUT_MS || 30000)
170
+ },
171
+
172
+ minimax: {
173
+ id: 'minimax',
174
+ name: 'MiniMax',
175
+ baseUrl: (env.MINIMAX_BASE_URL || 'https://api.minimax.chat/v1').replace(/\/$/, ''),
176
+ chatModel: env.MINIMAX_MODEL || 'abab6.5s-chat',
177
+ embedModel: env.MINIMAX_EMBED_MODEL || '',
178
+ apiKey: env.MINIMAX_API_KEY || '',
179
+ userAgent: '',
180
+ authHeaders: (key) => ({ Authorization: `Bearer ${key}` }),
181
+ chatPath: '/chat/completions',
182
+ embedPath: '/embeddings',
183
+ formatRequest: standardFormat,
184
+ parseResponse: standardParse,
185
+ timeout: Number(env.MINIMAX_TIMEOUT_MS || 30000)
186
+ },
187
+
188
+ claude: {
189
+ id: 'claude',
190
+ name: 'Claude (Anthropic)',
191
+ baseUrl: (env.CLAUDE_BASE_URL || 'https://api.anthropic.com').replace(/\/$/, ''),
192
+ chatModel: env.CLAUDE_MODEL || 'claude-sonnet-4-6',
193
+ embedModel: '',
194
+ apiKey: env.CLAUDE_API_KEY || env.ANTHROPIC_API_KEY || '',
195
+ userAgent: '',
196
+ authHeaders: (key) => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }),
197
+ chatPath: '/v1/messages',
198
+ embedPath: null, // Claude does not expose an embeddings endpoint
199
+ formatRequest: claudeFormat,
200
+ parseResponse: claudeParse,
201
+ timeout: Number(env.CLAUDE_TIMEOUT_MS || 60000)
202
+ }
203
+ };
204
+ }
205
+
206
+ // Singleton — providers are built once from process.env on first access
207
+ let _providers = null;
208
+ function getProviders() {
209
+ if (!_providers) _providers = buildProviders(process.env);
210
+ return _providers;
211
+ }
212
+
213
+ function getProvider(id) {
214
+ return getProviders()[id] || null;
215
+ }
216
+
217
+ module.exports = { getProviders, getProvider };
package/src/cli.js ADDED
@@ -0,0 +1,187 @@
1
+ #!/usr/bin/env node
2
+ const fs = require('fs');
3
+ const path = require('path');
4
+ const readline = require('readline');
5
+ const { spawn } = require('child_process');
6
+ const os = require('os');
7
+ const { ensureContextFiles } = require('./agent/contextFiles');
8
+ const { startReflectionScheduler } = require('./agent/reflectionScheduler');
9
+ const { initVectorMemory } = require('./agent/db');
10
+ const { startTelegramBot } = require('./telegram/bot');
11
+ const { handleMessage } = require('./agent/mainAgent');
12
+
13
+ // Source root — always inside the npm package
14
+ const srcRoot = path.resolve(__dirname, '..');
15
+ // Runtime root — inside TIGER_HOME when installed globally, otherwise project root
16
+ const rootDir = process.env.TIGER_HOME || process.cwd();
17
+ const supervisorPidPath = path.resolve(rootDir, 'tiger-telegram.pid');
18
+
19
+ process.on('unhandledRejection', (reason) => {
20
+ const msg = reason && reason.stack ? reason.stack : String(reason);
21
+ process.stderr.write(`[process] unhandledRejection: ${msg}\n`);
22
+ });
23
+
24
+ process.on('uncaughtException', (err) => {
25
+ process.stderr.write(`[process] uncaughtException: ${err.stack || err.message}\n`);
26
+ });
27
+
28
+ function isTelegramMode(argv) {
29
+ return argv.includes('--telegram');
30
+ }
31
+
32
+ function hasFlag(argv, flag) {
33
+ return argv.includes(flag);
34
+ }
35
+
36
+ function isPidRunning(pid) {
37
+ if (!pid) return false;
38
+ try {
39
+ process.kill(pid, 0);
40
+ return true;
41
+ } catch (err) {
42
+ return false;
43
+ }
44
+ }
45
+
46
+ function getExistingSupervisorPid() {
47
+ if (!fs.existsSync(supervisorPidPath)) return null;
48
+ const pid = Number(fs.readFileSync(supervisorPidPath, 'utf8').trim());
49
+ return Number.isInteger(pid) && pid > 0 ? pid : null;
50
+ }
51
+
52
+ function startTelegramInBackground() {
53
+ ensureContextFiles();
54
+ const existingPid = getExistingSupervisorPid();
55
+ if (existingPid && isPidRunning(existingPid)) {
56
+ process.stdout.write(`Telegram background bot is already running (PID ${existingPid}).\n`);
57
+ return;
58
+ }
59
+
60
+ fs.mkdirSync(path.resolve(rootDir, 'logs'), { recursive: true });
61
+ const supervisorLogPath = path.resolve(rootDir, 'logs', 'telegram-supervisor.log');
62
+ const logFd = fs.openSync(supervisorLogPath, 'a');
63
+ const supervisorScript = path.resolve(srcRoot, 'src', 'telegram', 'supervisor.js');
64
+ const child = spawn(process.execPath, [supervisorScript], {
65
+ cwd: rootDir,
66
+ env: process.env,
67
+ detached: true,
68
+ stdio: ['ignore', logFd, logFd]
69
+ });
70
+ child.unref();
71
+ fs.closeSync(logFd);
72
+
73
+ fs.writeFileSync(supervisorPidPath, `${child.pid}\n`, 'utf8');
74
+ process.stdout.write(`Telegram background bot started (supervisor PID ${child.pid}).\n`);
75
+ }
76
+
77
+ function stopTelegramBackground() {
78
+ const pid = getExistingSupervisorPid();
79
+ if (!pid || !isPidRunning(pid)) {
80
+ if (fs.existsSync(supervisorPidPath)) fs.unlinkSync(supervisorPidPath);
81
+ process.stdout.write('Telegram background bot is not running.\n');
82
+ return;
83
+ }
84
+
85
+ process.kill(pid, 'SIGTERM');
86
+ fs.unlinkSync(supervisorPidPath);
87
+ const workerPidPath = path.resolve(rootDir, 'tiger-telegram-worker.pid');
88
+ if (fs.existsSync(workerPidPath)) fs.unlinkSync(workerPidPath);
89
+ process.stdout.write(`Stopped Telegram background bot (supervisor PID ${pid}).\n`);
90
+ }
91
+
92
+ function printVectorMemoryStatus(vectorStatus) {
93
+ if (vectorStatus.ok) {
94
+ const vecMode = vectorStatus.sqliteVecLoaded ? 'enabled' : 'not loaded';
95
+ const count = Number(vectorStatus?.counts?.memories || 0);
96
+ process.stdout.write(
97
+ `Vector memory: sqlite (${vectorStatus.dbPath}) | sqlite-vec: ${vecMode} | memories: ${count}\n`
98
+ );
99
+ if (String(vectorStatus.dbPath || '').startsWith(`${os.tmpdir()}${path.sep}`)) {
100
+ process.stdout.write(
101
+ 'Warning: VECTOR_DB_PATH is under /tmp and may be wiped after restart. Use ./db/memory.sqlite for persistence.\n'
102
+ );
103
+ }
104
+ if (!vectorStatus.sqliteVecLoaded) {
105
+ process.stdout.write(
106
+ 'Info: sqlite-vec not loaded. Semantic recall still works using cosine fallback.\n'
107
+ );
108
+ }
109
+ return;
110
+ }
111
+ process.stdout.write(`Vector memory: json fallback (${vectorStatus.dbPath})\n`);
112
+ }
113
+
114
+ async function runCli() {
115
+ ensureContextFiles();
116
+ startReflectionScheduler();
117
+ const vectorStatus = initVectorMemory();
118
+ printVectorMemoryStatus(vectorStatus);
119
+
120
+ const rl = readline.createInterface({
121
+ input: process.stdin,
122
+ output: process.stdout,
123
+ prompt: 'you> '
124
+ });
125
+
126
+ const userId = 'local-user';
127
+ process.stdout.write('Tiger agent ready. Type /exit to quit.\n');
128
+ rl.prompt();
129
+
130
+ rl.on('line', async (line) => {
131
+ const text = String(line || '').trim();
132
+ if (!text) {
133
+ rl.prompt();
134
+ return;
135
+ }
136
+ if (text === '/exit' || text === '/quit') {
137
+ rl.close();
138
+ return;
139
+ }
140
+
141
+ try {
142
+ const reply = await handleMessage({
143
+ platform: 'cli',
144
+ userId,
145
+ text
146
+ });
147
+ process.stdout.write(`tiger> ${reply}\n`);
148
+ } catch (err) {
149
+ process.stdout.write(`error> ${err.message}\n`);
150
+ }
151
+ rl.prompt();
152
+ });
153
+
154
+ rl.on('close', () => {
155
+ process.stdout.write('bye\n');
156
+ process.exit(0);
157
+ });
158
+ }
159
+
160
+ async function main() {
161
+ const argv = process.argv.slice(2);
162
+ if (hasFlag(argv, '--telegram-stop')) {
163
+ stopTelegramBackground();
164
+ return;
165
+ }
166
+
167
+ if (isTelegramMode(argv) && hasFlag(argv, '--background')) {
168
+ startTelegramInBackground();
169
+ return;
170
+ }
171
+
172
+ if (isTelegramMode(argv)) {
173
+ ensureContextFiles();
174
+ startReflectionScheduler();
175
+ const vectorStatus = initVectorMemory();
176
+ printVectorMemoryStatus(vectorStatus);
177
+ startTelegramBot();
178
+ process.stdout.write('Telegram bot started.\n');
179
+ return;
180
+ }
181
+ await runCli();
182
+ }
183
+
184
+ main().catch((err) => {
185
+ process.stderr.write(`${err.stack || err.message}\n`);
186
+ process.exit(1);
187
+ });
package/src/config.js ADDED
@@ -0,0 +1,141 @@
1
+ const path = require('path');
2
+ const dotenv = require('dotenv');
3
+ const fs = require('fs');
4
+ const { decryptToString } = require('../scripts/cryptoEnv');
5
+
6
+ function loadDotenvIfPresent(p) {
7
+ const full = path.resolve(process.cwd(), p);
8
+ if (!fs.existsSync(full)) return;
9
+ dotenv.config({ path: full, override: false });
10
+ }
11
+
12
+ function parseEnvText(text) {
13
+ const out = {};
14
+ const lines = String(text || '').split(/\r?\n/);
15
+ for (const line of lines) {
16
+ const trimmed = line.trim();
17
+ if (!trimmed || trimmed.startsWith('#')) continue;
18
+ const idx = trimmed.indexOf('=');
19
+ if (idx === -1) continue;
20
+ const k = trimmed.slice(0, idx).trim();
21
+ let v = trimmed.slice(idx + 1).trim();
22
+ // remove surrounding quotes
23
+ v = v.replace(/^['"]|['"]$/g, '');
24
+ if (k) out[k] = v;
25
+ }
26
+ return out;
27
+ }
28
+
29
+ function loadEncryptedSecretsIfPresent() {
30
+ const encPath = process.env.SECRETS_FILE || '.env.secrets.enc';
31
+ const passphrase = process.env.SECRETS_PASSPHRASE || '';
32
+ const full = path.resolve(process.cwd(), encPath);
33
+ if (!fs.existsSync(full)) return;
34
+ if (!passphrase) return;
35
+ const payload = JSON.parse(fs.readFileSync(full, 'utf8'));
36
+ const plaintext = decryptToString(payload, passphrase);
37
+ const kv = parseEnvText(plaintext);
38
+ for (const [k, v] of Object.entries(kv)) {
39
+ if (process.env[k] == null || process.env[k] === '') {
40
+ process.env[k] = v;
41
+ }
42
+ }
43
+ }
44
+
45
+ // Load public config first, then local secrets, then encrypted secrets (optional).
46
+ loadDotenvIfPresent('.env');
47
+ loadDotenvIfPresent('.env.secrets');
48
+ loadEncryptedSecretsIfPresent();
49
+
50
+ function cleanEnvValue(value) {
51
+ if (value == null) return '';
52
+ return String(value).trim().replace(/^['"]|['"]$/g, '');
53
+ }
54
+
55
+ function normalizeModelForProvider(provider, model) {
56
+ const text = cleanEnvValue(model);
57
+ if (!text) return text;
58
+ if (!text.includes('/')) return text;
59
+
60
+ if (provider === 'code' && text.toLowerCase().startsWith('kimi-coding/')) {
61
+ return text.slice('kimi-coding/'.length);
62
+ }
63
+ if (provider === 'moonshot' && text.toLowerCase().startsWith('moonshot/')) {
64
+ return text.slice('moonshot/'.length);
65
+ }
66
+ return text;
67
+ }
68
+
69
+ const providerRaw = cleanEnvValue(process.env.KIMI_PROVIDER || '').toLowerCase();
70
+ const kimiProvider =
71
+ providerRaw || (cleanEnvValue(process.env.KIMI_CODE_API_KEY) ? 'code' : 'moonshot');
72
+
73
+ if (!['moonshot', 'code'].includes(kimiProvider)) {
74
+ throw new Error('Invalid KIMI_PROVIDER. Use "moonshot" or "code".');
75
+ }
76
+
77
+ const rawApiKey =
78
+ kimiProvider === 'code'
79
+ ? process.env.KIMI_CODE_API_KEY || process.env.KIMI_API_KEY || ''
80
+ : process.env.MOONSHOT_API_KEY || process.env.KIMI_API_KEY || '';
81
+ const kimiApiKey = cleanEnvValue(rawApiKey);
82
+ // Only hard-fail on missing kimi key when kimi/moonshot is the active provider.
83
+ // If ACTIVE_PROVIDER is set to another provider, the key is optional.
84
+ const activeProvider = cleanEnvValue(process.env.ACTIVE_PROVIDER || '').toLowerCase();
85
+ const kimiIsActive = !activeProvider || activeProvider === 'kimi' || activeProvider === 'moonshot';
86
+ if (!kimiApiKey && kimiIsActive) {
87
+ if (kimiProvider === 'code') {
88
+ throw new Error('Missing required env: KIMI_CODE_API_KEY (or KIMI_API_KEY)');
89
+ }
90
+ throw new Error('Missing required env: MOONSHOT_API_KEY (or KIMI_API_KEY)');
91
+ }
92
+
93
+ const defaultBaseUrl =
94
+ kimiProvider === 'code' ? 'https://api.kimi.com/coding/v1' : 'https://api.moonshot.cn/v1';
95
+ const defaultChatModel = kimiProvider === 'code' ? 'k2p5' : 'kimi-k1';
96
+ const defaultEmbedModel = kimiProvider === 'code' ? '' : 'kimi-embedding-v1';
97
+ const defaultUserAgent = kimiProvider === 'code' ? 'KimiCLI/0.77' : '';
98
+
99
+ const embedFlagRaw = cleanEnvValue(process.env.KIMI_ENABLE_EMBEDDINGS || '');
100
+ const embeddingsEnabled =
101
+ embedFlagRaw === ''
102
+ ? kimiProvider !== 'code'
103
+ : ['1', 'true', 'yes', 'on'].includes(embedFlagRaw.toLowerCase());
104
+ const ownSkillUpdateHours = Math.max(1, Number(process.env.OWN_SKILL_UPDATE_HOURS || 24));
105
+ const ownSkillFile = cleanEnvValue(process.env.OWN_SKILL_FILE) || 'ownskill.md';
106
+ const soulUpdateHours = Math.max(1, Number(process.env.SOUL_UPDATE_HOURS || 24));
107
+ const reflectionUpdateHours = Math.max(1, Number(process.env.REFLECTION_UPDATE_HOURS || 12));
108
+ const vectorDbPath = path.resolve(process.env.VECTOR_DB_PATH || './db/memory.sqlite');
109
+ const sqliteVecExtension = cleanEnvValue(process.env.SQLITE_VEC_EXTENSION || '');
110
+ const memoryIngestEveryTurns = Math.max(1, Number(process.env.MEMORY_INGEST_EVERY_TURNS || 2));
111
+ const memoryIngestMinChars = Math.max(20, Number(process.env.MEMORY_INGEST_MIN_CHARS || 140));
112
+
113
+ module.exports = {
114
+ kimiProvider,
115
+ kimiApiKey,
116
+ kimiBaseUrl: cleanEnvValue(process.env.KIMI_BASE_URL) || defaultBaseUrl,
117
+ kimiChatModel: normalizeModelForProvider(
118
+ kimiProvider,
119
+ cleanEnvValue(process.env.KIMI_CHAT_MODEL) || defaultChatModel
120
+ ),
121
+ kimiEmbedModel: cleanEnvValue(process.env.KIMI_EMBED_MODEL) || defaultEmbedModel,
122
+ kimiUserAgent: cleanEnvValue(process.env.KIMI_USER_AGENT) || defaultUserAgent,
123
+ kimiTimeoutMs: Number(process.env.KIMI_TIMEOUT_MS || 30000),
124
+ embeddingsEnabled,
125
+ telegramBotToken: process.env.TELEGRAM_BOT_TOKEN || '',
126
+ allowShell: String(process.env.ALLOW_SHELL || 'false').toLowerCase() === 'true',
127
+ allowSkillInstall: String(process.env.ALLOW_SKILL_INSTALL || 'false').toLowerCase() === 'true',
128
+ dataDir: path.resolve(process.env.DATA_DIR || './data'),
129
+ ownSkillPath: path.resolve(process.env.DATA_DIR || './data', ownSkillFile),
130
+ ownSkillUpdateHours,
131
+ soulPath: path.resolve(process.env.DATA_DIR || './data', 'soul.md'),
132
+ soulUpdateHours,
133
+ reflectionUpdateHours,
134
+ vectorDbPath,
135
+ sqliteVecExtension,
136
+ memoryIngestEveryTurns,
137
+ memoryIngestMinChars,
138
+ dbPath: path.resolve(process.env.DB_PATH || './db/agent.json'),
139
+ maxMessages: Number(process.env.MAX_MESSAGES || 200),
140
+ recentMessages: Number(process.env.RECENT_MESSAGES || 40)
141
+ };
@@ -0,0 +1,88 @@
1
+ const {
2
+ kimiApiKey,
3
+ kimiBaseUrl,
4
+ kimiChatModel,
5
+ kimiEmbedModel,
6
+ kimiUserAgent,
7
+ kimiTimeoutMs,
8
+ embeddingsEnabled,
9
+ kimiProvider
10
+ } = require('./config');
11
+
12
+ async function kimiRequest(path, body) {
13
+ const headers = {
14
+ 'Content-Type': 'application/json',
15
+ Authorization: `Bearer ${kimiApiKey}`
16
+ };
17
+ if (kimiUserAgent) {
18
+ headers['User-Agent'] = kimiUserAgent;
19
+ }
20
+
21
+ const controller = new AbortController();
22
+ const timer = setTimeout(() => controller.abort(), Math.max(1000, kimiTimeoutMs || 30000));
23
+ let res;
24
+ try {
25
+ res = await fetch(`${kimiBaseUrl}${path}`, {
26
+ method: 'POST',
27
+ headers,
28
+ body: JSON.stringify(body),
29
+ signal: controller.signal
30
+ });
31
+ } catch (err) {
32
+ if (err && err.name === 'AbortError') {
33
+ throw new Error(`Kimi API timeout after ${Math.max(1000, kimiTimeoutMs || 30000)}ms`);
34
+ }
35
+ throw new Error(`Kimi API network error: ${err.message}`);
36
+ } finally {
37
+ clearTimeout(timer);
38
+ }
39
+
40
+ if (!res.ok) {
41
+ const text = await res.text();
42
+ if (res.status === 401) {
43
+ const keyHint =
44
+ kimiProvider === 'code'
45
+ ? 'KIMI_CODE_API_KEY (Kimi Code token)'
46
+ : 'MOONSHOT_API_KEY/KIMI_API_KEY (Moonshot Open Platform key)';
47
+ throw new Error(
48
+ `Kimi API auth failed (401). Verify ${keyHint}, base URL (${kimiBaseUrl}), and extra quotes/spaces. Raw response: ${text}`
49
+ );
50
+ }
51
+ throw new Error(`Kimi API error ${res.status}: ${text}`);
52
+ }
53
+
54
+ return res.json();
55
+ }
56
+
57
+ async function chatCompletion(messages, options = {}) {
58
+ const payload = {
59
+ model: options.model || kimiChatModel,
60
+ messages,
61
+ temperature: options.temperature ?? 0.3
62
+ };
63
+ if (options.tools) payload.tools = options.tools;
64
+ if (options.tool_choice) payload.tool_choice = options.tool_choice;
65
+
66
+ const data = await kimiRequest('/chat/completions', payload);
67
+ return data.choices?.[0]?.message || {};
68
+ }
69
+
70
+ async function embedText(input, model = kimiEmbedModel) {
71
+ if (!embeddingsEnabled || !model) {
72
+ throw new Error('Embeddings are disabled for the current Kimi provider/config.');
73
+ }
74
+ const data = await kimiRequest('/embeddings', {
75
+ model,
76
+ input
77
+ });
78
+ const vector = data.data?.[0]?.embedding;
79
+ if (!vector || !Array.isArray(vector)) {
80
+ throw new Error('Invalid embedding response from Kimi');
81
+ }
82
+ return vector;
83
+ }
84
+
85
+ module.exports = {
86
+ chatCompletion,
87
+ embedText
88
+ };