tiger-agent 0.2.5 → 0.3.2

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.
@@ -60,6 +60,19 @@ function envLine(k, v) {
60
60
  return `${k}=${s}`;
61
61
  }
62
62
 
63
+ const KNOWN_PROVIDERS = ['minimax', 'zai', 'claude', 'kimi', 'moonshot'];
64
+
65
+ function parseProviderList(input, fallback = []) {
66
+ const raw = String(input || '').trim();
67
+ const values = (raw ? raw : fallback.join(','))
68
+ .split(',')
69
+ .map((s) => s.trim().toLowerCase())
70
+ .filter(Boolean);
71
+ const unique = [...new Set(values)];
72
+ const invalid = unique.filter((p) => !KNOWN_PROVIDERS.includes(p));
73
+ return { providers: unique, invalid };
74
+ }
75
+
63
76
  // ─── Daemon helpers ───────────────────────────────────────────────────────────
64
77
 
65
78
  function nodeBin() {
@@ -180,32 +193,87 @@ Config will be saved to: ${TIGER_HOME}
180
193
  if (!yn(ow, false)) { console.log('Cancelled.'); rl.close(); return; }
181
194
  }
182
195
 
183
- // ── Active provider ────────────────────────────────────────────────────────
184
- console.log('\nAvailable providers: kimi, zai (Zhipu GLM-4.7), minimax, claude, moonshot');
185
- const activeProv = (await ask('Active provider (zai): ')).trim() || 'zai';
186
- const provOrder = (await ask(`Provider fallback order (${activeProv},claude,kimi,minimax,moonshot): `)).trim()
187
- || `${activeProv},claude,kimi,minimax,moonshot`;
196
+ // ── Provider selection / routing ──────────────────────────────────────────
197
+ console.log('\nAvailable providers: minimax, zai (Zhipu GLM-4.7), claude, kimi, moonshot');
198
+ console.log('Choose only providers you want to configure. Others will be omitted from .env.');
199
+
200
+ let selectedProviders = [];
201
+ while (!selectedProviders.length) {
202
+ const picked = await ask('Providers to configure (comma-separated, default: minimax): ');
203
+ const parsed = parseProviderList(picked, ['minimax']);
204
+ if (parsed.invalid.length) {
205
+ console.log(`Invalid provider(s): ${parsed.invalid.join(', ')}. Try again.`);
206
+ continue;
207
+ }
208
+ if (!parsed.providers.length) {
209
+ console.log('Pick at least one provider.');
210
+ continue;
211
+ }
212
+ selectedProviders = parsed.providers;
213
+ }
214
+
215
+ const activeDefault = selectedProviders[0];
216
+ let activeProv = '';
217
+ while (!activeProv) {
218
+ const candidate = (await ask(`Active provider (${activeDefault}): `)).trim().toLowerCase() || activeDefault;
219
+ if (!selectedProviders.includes(candidate)) {
220
+ console.log(`Active provider must be one of: ${selectedProviders.join(', ')}`);
221
+ continue;
222
+ }
223
+ activeProv = candidate;
224
+ }
188
225
 
189
- // ── API keys ───────────────────────────────────────────────────────────────
190
- console.log('\nEnter API keys (press Enter to skip a provider):');
226
+ const orderDefault = [activeProv, ...selectedProviders.filter((p) => p !== activeProv)].join(',');
227
+ let provOrder = '';
228
+ while (!provOrder) {
229
+ const input = await ask(`Provider fallback order (${orderDefault}): `);
230
+ const parsed = parseProviderList(input, [activeProv, ...selectedProviders.filter((p) => p !== activeProv)]);
231
+ if (parsed.invalid.length) {
232
+ console.log(`Invalid provider(s): ${parsed.invalid.join(', ')}. Try again.`);
233
+ continue;
234
+ }
235
+ const outsideSelection = parsed.providers.filter((p) => !selectedProviders.includes(p));
236
+ if (outsideSelection.length) {
237
+ console.log(`Order can only include selected providers: ${selectedProviders.join(', ')}`);
238
+ continue;
239
+ }
240
+ if (!parsed.providers.includes(activeProv)) {
241
+ console.log(`Order must include active provider: ${activeProv}`);
242
+ continue;
243
+ }
244
+ provOrder = parsed.providers.join(',');
245
+ }
191
246
 
192
- const kimiKey = (await askHidden(' KIMI_CODE_API_KEY : ')).trim();
193
- const moonshotKey= (await askHidden(' MOONSHOT_API_KEY : ')).trim();
194
- const zaiKey = (await askHidden(' ZAI_API_KEY : ')).trim();
195
- const minimaxKey = (await askHidden(' MINIMAX_API_KEY : ')).trim();
196
- const claudeKey = (await askHidden(' CLAUDE_API_KEY : ')).trim();
247
+ // ── API keys ───────────────────────────────────────────────────────────────
248
+ console.log('\nEnter API keys for selected providers:');
249
+ const kimiKey = selectedProviders.includes('kimi') ? (await askHidden(' KIMI_CODE_API_KEY : ')).trim() : '';
250
+ const moonshotKey = selectedProviders.includes('moonshot') ? (await askHidden(' MOONSHOT_API_KEY : ')).trim() : '';
251
+ const zaiKey = selectedProviders.includes('zai') ? (await askHidden(' ZAI_API_KEY : ')).trim() : '';
252
+ const minimaxKey = selectedProviders.includes('minimax') ? (await askHidden(' MINIMAX_API_KEY : ')).trim() : '';
253
+ const claudeKey = selectedProviders.includes('claude') ? (await askHidden(' CLAUDE_API_KEY : ')).trim() : '';
197
254
 
198
255
  // ── Telegram ───────────────────────────────────────────────────────────────
199
256
  console.log('');
200
257
  const tgToken = (await askHidden(' TELEGRAM_BOT_TOKEN : ')).trim();
201
258
 
202
259
  // ── Token limits ───────────────────────────────────────────────────────────
203
- console.log('\nDaily token limits per provider (0 = unlimited, auto-switch on breach):');
204
- const kimiLimit = (await ask(' KIMI_TOKEN_LIMIT (100000): ')).trim() || '100000';
205
- const moonshotLimit= (await ask(' MOONSHOT_TOKEN_LIMIT(100000): ')).trim() || '100000';
206
- const zaiLimit = (await ask(' ZAI_TOKEN_LIMIT (100000): ')).trim() || '100000';
207
- const minimaxLimit = (await ask(' MINIMAX_TOKEN_LIMIT (100000): ')).trim() || '100000';
208
- const claudeLimit = (await ask(' CLAUDE_TOKEN_LIMIT (500000): ')).trim() || '500000';
260
+ const tokenLimits = {};
261
+ console.log('\nDaily token limits for selected providers (0 = unlimited, auto-switch on breach):');
262
+ if (selectedProviders.includes('kimi')) {
263
+ tokenLimits.kimi = (await ask(' KIMI_TOKEN_LIMIT (100000): ')).trim() || '100000';
264
+ }
265
+ if (selectedProviders.includes('moonshot')) {
266
+ tokenLimits.moonshot = (await ask(' MOONSHOT_TOKEN_LIMIT(100000): ')).trim() || '100000';
267
+ }
268
+ if (selectedProviders.includes('zai')) {
269
+ tokenLimits.zai = (await ask(' ZAI_TOKEN_LIMIT (100000): ')).trim() || '100000';
270
+ }
271
+ if (selectedProviders.includes('minimax')) {
272
+ tokenLimits.minimax = (await ask(' MINIMAX_TOKEN_LIMIT (100000): ')).trim() || '100000';
273
+ }
274
+ if (selectedProviders.includes('claude')) {
275
+ tokenLimits.claude = (await ask(' CLAUDE_TOKEN_LIMIT (500000): ')).trim() || '500000';
276
+ }
209
277
 
210
278
  // ── Misc ───────────────────────────────────────────────────────────────────
211
279
  const allowShell = yn(await ask('\nEnable shell tool? (y/N): '), false);
@@ -215,51 +283,80 @@ Config will be saved to: ${TIGER_HOME}
215
283
  const lines = [
216
284
  '# Tiger Agent config — generated by `tiger onboard`',
217
285
  '',
218
- '# ── Legacy Kimi compat (used if ACTIVE_PROVIDER=kimi)',
219
- envLine('KIMI_PROVIDER', 'code'),
220
- envLine('KIMI_CODE_API_KEY', kimiKey),
221
- envLine('KIMI_BASE_URL', 'https://api.kimi.com/coding/v1'),
222
- envLine('KIMI_CHAT_MODEL', 'kimi-coding/k2p5'),
223
- envLine('KIMI_EMBED_MODEL', ''),
224
- envLine('KIMI_USER_AGENT', 'KimiCLI/0.77'),
225
- envLine('KIMI_ENABLE_EMBEDDINGS', 'false'),
226
- envLine('KIMI_TIMEOUT_MS', '30000'),
227
- '',
228
286
  '# ── Multi-provider',
229
287
  envLine('ACTIVE_PROVIDER', activeProv),
230
288
  envLine('PROVIDER_ORDER', provOrder),
231
- '',
232
- '# ── Z.ai (Zhipu GLM)',
233
- envLine('ZAI_API_KEY', zaiKey),
234
- envLine('ZAI_BASE_URL', 'https://api.z.ai/api/coding/paas/v4'),
235
- envLine('ZAI_MODEL', 'glm-4.7'),
236
- envLine('ZAI_TIMEOUT_MS', '30000'),
237
- '',
238
- '# ── MiniMax',
239
- envLine('MINIMAX_API_KEY', minimaxKey),
240
- envLine('MINIMAX_BASE_URL', 'https://api.minimax.chat/v1'),
241
- envLine('MINIMAX_MODEL', 'abab6.5s-chat'),
242
- envLine('MINIMAX_TIMEOUT_MS', '30000'),
243
- '',
244
- '# ── Claude (Anthropic)',
245
- envLine('CLAUDE_API_KEY', claudeKey),
246
- envLine('CLAUDE_MODEL', 'claude-sonnet-4-6'),
247
- envLine('CLAUDE_TIMEOUT_MS', '60000'),
248
- '',
249
- '# ── Moonshot',
250
- envLine('MOONSHOT_API_KEY', moonshotKey),
251
- envLine('MOONSHOT_BASE_URL', 'https://api.moonshot.cn/v1'),
252
- envLine('MOONSHOT_MODEL', 'kimi-k1'),
253
- '',
254
- '# ── Token limits (daily, 0 = unlimited)',
255
- envLine('KIMI_TOKEN_LIMIT', kimiLimit),
256
- envLine('MOONSHOT_TOKEN_LIMIT', moonshotLimit),
257
- envLine('ZAI_TOKEN_LIMIT', zaiLimit),
258
- envLine('MINIMAX_TOKEN_LIMIT', minimaxLimit),
259
- envLine('CLAUDE_TOKEN_LIMIT', claudeLimit),
289
+ ''
290
+ ];
291
+
292
+ if (selectedProviders.includes('kimi')) {
293
+ lines.push(
294
+ '# ── Legacy Kimi compat (used if ACTIVE_PROVIDER=kimi)',
295
+ envLine('KIMI_PROVIDER', 'code'),
296
+ envLine('KIMI_CODE_API_KEY', kimiKey),
297
+ envLine('KIMI_BASE_URL', 'https://api.kimi.com/coding/v1'),
298
+ envLine('KIMI_CHAT_MODEL', 'kimi-coding/k2p5'),
299
+ envLine('KIMI_EMBED_MODEL', ''),
300
+ envLine('KIMI_USER_AGENT', 'KimiCLI/0.77'),
301
+ envLine('KIMI_ENABLE_EMBEDDINGS', 'false'),
302
+ envLine('KIMI_TIMEOUT_MS', '30000'),
303
+ ''
304
+ );
305
+ }
306
+
307
+ if (selectedProviders.includes('zai')) {
308
+ lines.push(
309
+ '# ── Z.ai (Zhipu GLM)',
310
+ envLine('ZAI_API_KEY', zaiKey),
311
+ envLine('ZAI_BASE_URL', 'https://api.z.ai/api/coding/paas/v4'),
312
+ envLine('ZAI_MODEL', 'glm-4.7'),
313
+ envLine('ZAI_TIMEOUT_MS', '30000'),
314
+ ''
315
+ );
316
+ }
317
+
318
+ if (selectedProviders.includes('minimax')) {
319
+ lines.push(
320
+ '# ── MiniMax (Coding / OpenAI-compatible)',
321
+ envLine('MINIMAX_API_KEY', minimaxKey),
322
+ envLine('MINIMAX_BASE_URL', 'https://api.minimax.io/v1'),
323
+ envLine('MINIMAX_MODEL', 'MiniMax-M2.5'),
324
+ envLine('MINIMAX_TIMEOUT_MS', '30000'),
325
+ ''
326
+ );
327
+ }
328
+
329
+ if (selectedProviders.includes('claude')) {
330
+ lines.push(
331
+ '# ── Claude (Anthropic)',
332
+ envLine('CLAUDE_API_KEY', claudeKey),
333
+ envLine('CLAUDE_MODEL', 'claude-sonnet-4-6'),
334
+ envLine('CLAUDE_TIMEOUT_MS', '60000'),
335
+ ''
336
+ );
337
+ }
338
+
339
+ if (selectedProviders.includes('moonshot')) {
340
+ lines.push(
341
+ '# ── Moonshot',
342
+ envLine('MOONSHOT_API_KEY', moonshotKey),
343
+ envLine('MOONSHOT_BASE_URL', 'https://api.moonshot.cn/v1'),
344
+ envLine('MOONSHOT_MODEL', 'kimi-k1'),
345
+ ''
346
+ );
347
+ }
348
+
349
+ lines.push('# ── Token limits (daily, 0 = unlimited)');
350
+ if (tokenLimits.kimi != null) lines.push(envLine('KIMI_TOKEN_LIMIT', tokenLimits.kimi));
351
+ if (tokenLimits.moonshot != null) lines.push(envLine('MOONSHOT_TOKEN_LIMIT', tokenLimits.moonshot));
352
+ if (tokenLimits.zai != null) lines.push(envLine('ZAI_TOKEN_LIMIT', tokenLimits.zai));
353
+ if (tokenLimits.minimax != null) lines.push(envLine('MINIMAX_TOKEN_LIMIT', tokenLimits.minimax));
354
+ if (tokenLimits.claude != null) lines.push(envLine('CLAUDE_TOKEN_LIMIT', tokenLimits.claude));
355
+ lines.push(
260
356
  '',
261
357
  '# ── Telegram',
262
358
  envLine('TELEGRAM_BOT_TOKEN', tgToken),
359
+ envLine('SWARM_ENABLED', 'false'),
263
360
  '',
264
361
  '# ── Permissions',
265
362
  envLine('ALLOW_SHELL', allowShell ? 'true' : 'false'),
@@ -280,7 +377,7 @@ Config will be saved to: ${TIGER_HOME}
280
377
  'MEMORY_INGEST_EVERY_TURNS=2',
281
378
  'MEMORY_INGEST_MIN_CHARS=140',
282
379
  ''
283
- ];
380
+ );
284
381
 
285
382
  fs.writeFileSync(ENV_PATH, lines.join('\n'), { mode: 0o600 });
286
383
  console.log(`\n✅ Config written to ${ENV_PATH}`);
@@ -307,7 +404,7 @@ Setup complete! 🐯
307
404
  Start CLI: tiger start
308
405
  Start Telegram: tiger telegram
309
406
  Background daemon: tiger telegram --background
310
- Switch provider: /api claude (in Telegram chat)
407
+ Switch provider: /api <provider_id> (in Telegram chat)
311
408
  Token usage: /tokens (in Telegram chat)
312
409
  ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━`);
313
410
 
@@ -135,7 +135,7 @@ async function clawhubInstall(args = {}) {
135
135
  if (force) argv.push('--force');
136
136
 
137
137
  const res = await runClawhub(argv, {
138
- timeout: Number(args.timeout_ms || 120000),
138
+ timeout: Number(args.timeout_ms || process.env.SWARM_AGENT_TIMEOUT_MS || 720000),
139
139
  maxBuffer: 1024 * 1024
140
140
  });
141
141
  if (res.ok) {
@@ -134,7 +134,7 @@ function buildProviders(env) {
134
134
  embedPath: '/embeddings',
135
135
  formatRequest: standardFormat,
136
136
  parseResponse: standardParse,
137
- timeout: Number(env.KIMI_TIMEOUT_MS || 30000)
137
+ timeout: Number(env.KIMI_TIMEOUT_MS || 180000)
138
138
  },
139
139
 
140
140
  moonshot: {
@@ -150,7 +150,7 @@ function buildProviders(env) {
150
150
  embedPath: '/embeddings',
151
151
  formatRequest: standardFormat,
152
152
  parseResponse: standardParse,
153
- timeout: Number(env.KIMI_TIMEOUT_MS || 30000)
153
+ timeout: Number(env.KIMI_TIMEOUT_MS || 180000)
154
154
  },
155
155
 
156
156
  zai: {
@@ -171,14 +171,14 @@ function buildProviders(env) {
171
171
  embedPath: '/embeddings',
172
172
  formatRequest: standardFormat,
173
173
  parseResponse: standardParse,
174
- timeout: Number(env.ZAI_TIMEOUT_MS || 30000)
174
+ timeout: Number(env.ZAI_TIMEOUT_MS || 180000)
175
175
  },
176
176
 
177
177
  minimax: {
178
178
  id: 'minimax',
179
179
  name: 'MiniMax',
180
- baseUrl: (env.MINIMAX_BASE_URL || 'https://api.minimax.chat/v1').replace(/\/$/, ''),
181
- chatModel: env.MINIMAX_MODEL || 'abab6.5s-chat',
180
+ baseUrl: (env.MINIMAX_BASE_URL || 'https://api.minimax.io/v1').replace(/\/$/, ''),
181
+ chatModel: env.MINIMAX_MODEL || 'MiniMax-M2.5',
182
182
  embedModel: env.MINIMAX_EMBED_MODEL || '',
183
183
  apiKey: env.MINIMAX_API_KEY || '',
184
184
  userAgent: '',
@@ -187,7 +187,7 @@ function buildProviders(env) {
187
187
  embedPath: '/embeddings',
188
188
  formatRequest: standardFormat,
189
189
  parseResponse: standardParse,
190
- timeout: Number(env.MINIMAX_TIMEOUT_MS || 30000)
190
+ timeout: Number(env.MINIMAX_TIMEOUT_MS || 180000)
191
191
  },
192
192
 
193
193
  claude: {
@@ -203,16 +203,15 @@ function buildProviders(env) {
203
203
  embedPath: null, // Claude does not expose an embeddings endpoint
204
204
  formatRequest: claudeFormat,
205
205
  parseResponse: claudeParse,
206
- timeout: Number(env.CLAUDE_TIMEOUT_MS || 60000)
206
+ timeout: Number(env.CLAUDE_TIMEOUT_MS || 180000)
207
207
  }
208
208
  };
209
209
  }
210
210
 
211
- // Singleton providers are built once from process.env on first access
212
- let _providers = null;
211
+ // Always rebuild from current process.env so runtime .env changes take effect
212
+ // (fixes stale timeout after PM2 restart or .env update)
213
213
  function getProviders() {
214
- if (!_providers) _providers = buildProviders(process.env);
215
- return _providers;
214
+ return buildProviders(process.env);
216
215
  }
217
216
 
218
217
  function getProvider(id) {
@@ -0,0 +1,222 @@
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',
159
+ baseUrl: (env.ZAI_BASE_URL || 'https://api.z.ai/api/coding/paas/v4').replace(/\/$/, ''),
160
+ chatModel: env.ZAI_MODEL || 'glm-4.7',
161
+ embedModel: env.ZAI_EMBED_MODEL || '',
162
+ apiKey: env.ZAI_API_KEY || '',
163
+ userAgent: '',
164
+ // api.z.ai uses plain Bearer; old bigmodel.cn used Zhipu JWT
165
+ authHeaders: (key) => {
166
+ const baseUrl = (env.ZAI_BASE_URL || '').toLowerCase();
167
+ if (baseUrl.includes('bigmodel.cn')) return { Authorization: `Bearer ${zhipuJwt(key)}` };
168
+ return { Authorization: `Bearer ${key}` };
169
+ },
170
+ chatPath: '/chat/completions',
171
+ embedPath: '/embeddings',
172
+ formatRequest: standardFormat,
173
+ parseResponse: standardParse,
174
+ timeout: Number(env.ZAI_TIMEOUT_MS || 30000)
175
+ },
176
+
177
+ minimax: {
178
+ id: 'minimax',
179
+ name: 'MiniMax',
180
+ baseUrl: (env.MINIMAX_BASE_URL || 'https://api.minimax.chat/v1').replace(/\/$/, ''),
181
+ chatModel: env.MINIMAX_MODEL || 'abab6.5s-chat',
182
+ embedModel: env.MINIMAX_EMBED_MODEL || '',
183
+ apiKey: env.MINIMAX_API_KEY || '',
184
+ userAgent: '',
185
+ authHeaders: (key) => ({ Authorization: `Bearer ${key}` }),
186
+ chatPath: '/chat/completions',
187
+ embedPath: '/embeddings',
188
+ formatRequest: standardFormat,
189
+ parseResponse: standardParse,
190
+ timeout: Number(env.MINIMAX_TIMEOUT_MS || 30000)
191
+ },
192
+
193
+ claude: {
194
+ id: 'claude',
195
+ name: 'Claude (Anthropic)',
196
+ baseUrl: (env.CLAUDE_BASE_URL || 'https://api.anthropic.com').replace(/\/$/, ''),
197
+ chatModel: env.CLAUDE_MODEL || 'claude-sonnet-4-6',
198
+ embedModel: '',
199
+ apiKey: env.CLAUDE_API_KEY || env.ANTHROPIC_API_KEY || '',
200
+ userAgent: '',
201
+ authHeaders: (key) => ({ 'x-api-key': key, 'anthropic-version': '2023-06-01' }),
202
+ chatPath: '/v1/messages',
203
+ embedPath: null, // Claude does not expose an embeddings endpoint
204
+ formatRequest: claudeFormat,
205
+ parseResponse: claudeParse,
206
+ timeout: Number(env.CLAUDE_TIMEOUT_MS || 60000)
207
+ }
208
+ };
209
+ }
210
+
211
+ // Singleton — providers are built once from process.env on first access
212
+ let _providers = null;
213
+ function getProviders() {
214
+ if (!_providers) _providers = buildProviders(process.env);
215
+ return _providers;
216
+ }
217
+
218
+ function getProvider(id) {
219
+ return getProviders()[id] || null;
220
+ }
221
+
222
+ module.exports = { getProviders, getProvider };
package/src/cli.js CHANGED
@@ -9,12 +9,14 @@ const { startReflectionScheduler } = require('./agent/reflectionScheduler');
9
9
  const { initVectorMemory } = require('./agent/db');
10
10
  const { startTelegramBot } = require('./telegram/bot');
11
11
  const { handleMessage } = require('./agent/mainAgent');
12
+ const { ensureSwarmLayout } = require('./swarm');
12
13
 
13
14
  // Source root — always inside the npm package
14
15
  const srcRoot = path.resolve(__dirname, '..');
15
16
  // Runtime root — inside TIGER_HOME when installed globally, otherwise project root
16
17
  const rootDir = process.env.TIGER_HOME || process.cwd();
17
18
  const supervisorPidPath = path.resolve(rootDir, 'tiger-telegram.pid');
19
+ const workerHeartbeatPath = path.resolve(rootDir, 'tiger-telegram-worker.heartbeat');
18
20
 
19
21
  process.on('unhandledRejection', (reason) => {
20
22
  const msg = reason && reason.stack ? reason.stack : String(reason);
@@ -33,6 +35,14 @@ function hasFlag(argv, flag) {
33
35
  return argv.includes(flag);
34
36
  }
35
37
 
38
+ function writeWorkerHeartbeat() {
39
+ try {
40
+ fs.writeFileSync(workerHeartbeatPath, `${Date.now()}\n`, 'utf8');
41
+ } catch (err) {
42
+ // Heartbeat is best-effort and must not crash the worker.
43
+ }
44
+ }
45
+
36
46
  function isPidRunning(pid) {
37
47
  if (!pid) return false;
38
48
  try {
@@ -51,6 +61,7 @@ function getExistingSupervisorPid() {
51
61
 
52
62
  function startTelegramInBackground() {
53
63
  ensureContextFiles();
64
+ ensureSwarmLayout();
54
65
  const existingPid = getExistingSupervisorPid();
55
66
  if (existingPid && isPidRunning(existingPid)) {
56
67
  process.stdout.write(`Telegram background bot is already running (PID ${existingPid}).\n`);
@@ -85,7 +96,9 @@ function stopTelegramBackground() {
85
96
  process.kill(pid, 'SIGTERM');
86
97
  fs.unlinkSync(supervisorPidPath);
87
98
  const workerPidPath = path.resolve(rootDir, 'tiger-telegram-worker.pid');
99
+ const workerHeartbeatPath = path.resolve(rootDir, 'tiger-telegram-worker.heartbeat');
88
100
  if (fs.existsSync(workerPidPath)) fs.unlinkSync(workerPidPath);
101
+ if (fs.existsSync(workerHeartbeatPath)) fs.unlinkSync(workerHeartbeatPath);
89
102
  process.stdout.write(`Stopped Telegram background bot (supervisor PID ${pid}).\n`);
90
103
  }
91
104
 
@@ -113,6 +126,7 @@ function printVectorMemoryStatus(vectorStatus) {
113
126
 
114
127
  async function runCli() {
115
128
  ensureContextFiles();
129
+ ensureSwarmLayout();
116
130
  startReflectionScheduler();
117
131
  const vectorStatus = initVectorMemory();
118
132
  printVectorMemoryStatus(vectorStatus);
@@ -159,6 +173,7 @@ async function runCli() {
159
173
 
160
174
  async function main() {
161
175
  const argv = process.argv.slice(2);
176
+ const isWorkerProcess = hasFlag(argv, '--worker');
162
177
  if (hasFlag(argv, '--telegram-stop')) {
163
178
  stopTelegramBackground();
164
179
  return;
@@ -171,9 +186,15 @@ async function main() {
171
186
 
172
187
  if (isTelegramMode(argv)) {
173
188
  ensureContextFiles();
189
+ ensureSwarmLayout();
174
190
  startReflectionScheduler();
175
191
  const vectorStatus = initVectorMemory();
176
192
  printVectorMemoryStatus(vectorStatus);
193
+ if (isWorkerProcess) {
194
+ // NanoClaw-style heartbeat: worker emits liveness every minute.
195
+ writeWorkerHeartbeat();
196
+ setInterval(writeWorkerHeartbeat, 60 * 1000);
197
+ }
177
198
  startTelegramBot();
178
199
  process.stdout.write('Telegram bot started.\n');
179
200
  return;