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.
- package/.env.example +22 -0
- package/.env.secrets.example +14 -0
- package/LICENSE +22 -0
- package/README.md +284 -0
- package/bin/tiger.js +96 -0
- package/package.json +58 -0
- package/scripts/audit.sh +54 -0
- package/scripts/backup.sh +42 -0
- package/scripts/cryptoEnv.js +57 -0
- package/scripts/decrypt-env.js +34 -0
- package/scripts/encrypt-env.js +34 -0
- package/scripts/migrate-vector-db.js +44 -0
- package/scripts/onboard.js +319 -0
- package/scripts/scan-secrets.sh +87 -0
- package/scripts/setup.js +302 -0
- package/scripts/sqlite_memory.py +297 -0
- package/scripts/sqlite_vec_setup.py +112 -0
- package/src/agent/contextFiles.js +30 -0
- package/src/agent/db.js +349 -0
- package/src/agent/mainAgent.js +406 -0
- package/src/agent/reflectionAgent.js +193 -0
- package/src/agent/reflectionScheduler.js +21 -0
- package/src/agent/skills.js +169 -0
- package/src/agent/subAgent.js +39 -0
- package/src/agent/toolbox.js +291 -0
- package/src/apiProviders.js +217 -0
- package/src/cli.js +187 -0
- package/src/config.js +141 -0
- package/src/kimiClient.js +88 -0
- package/src/llmClient.js +147 -0
- package/src/telegram/bot.js +182 -0
- package/src/telegram/supervisor.js +84 -0
- package/src/tokenManager.js +223 -0
- package/src/utils.js +30 -0
|
@@ -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
|
+
};
|