@teamvibe/poller 0.1.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/dist/auth-provider.d.ts +17 -0
- package/dist/auth-provider.js +62 -0
- package/dist/claude-spawner.d.ts +11 -0
- package/dist/claude-spawner.js +316 -0
- package/dist/config.d.ts +74 -0
- package/dist/config.js +48 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +238 -0
- package/dist/kb-manager.d.ts +15 -0
- package/dist/kb-manager.js +61 -0
- package/dist/logger.d.ts +18 -0
- package/dist/logger.js +97 -0
- package/dist/scripts/slack-tool.d.ts +1 -0
- package/dist/scripts/slack-tool.js +132 -0
- package/dist/session-store.d.ts +11 -0
- package/dist/session-store.js +221 -0
- package/dist/slack-client.d.ts +11 -0
- package/dist/slack-client.js +139 -0
- package/dist/sqs-poller.d.ts +10 -0
- package/dist/sqs-poller.js +86 -0
- package/dist/types.d.ts +58 -0
- package/dist/types.js +1 -0
- package/package.json +34 -0
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
interface AuthCredentials {
|
|
2
|
+
accessKeyId: string;
|
|
3
|
+
secretAccessKey: string;
|
|
4
|
+
sessionToken: string;
|
|
5
|
+
expiration: string;
|
|
6
|
+
}
|
|
7
|
+
interface AuthConfig {
|
|
8
|
+
sqsQueueUrl: string;
|
|
9
|
+
sessionsTable: string;
|
|
10
|
+
region: string;
|
|
11
|
+
}
|
|
12
|
+
export declare function initAuth(apiUrl: string, token: string): Promise<void>;
|
|
13
|
+
export declare function getCredentials(): AuthCredentials;
|
|
14
|
+
export declare function getAuthConfig(): AuthConfig;
|
|
15
|
+
export declare function isTokenMode(): boolean;
|
|
16
|
+
export declare function stopRefresh(): void;
|
|
17
|
+
export {};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { logger } from './logger.js';
|
|
2
|
+
let currentCredentials = null;
|
|
3
|
+
let currentConfig = null;
|
|
4
|
+
let refreshTimer = null;
|
|
5
|
+
const REFRESH_INTERVAL_MS = 50 * 60 * 1000; // 50 minutes (before 1hr expiry)
|
|
6
|
+
async function fetchCredentials(apiUrl, token) {
|
|
7
|
+
const response = await fetch(`${apiUrl}/auth`, {
|
|
8
|
+
method: 'POST',
|
|
9
|
+
headers: {
|
|
10
|
+
Authorization: `Bearer ${token}`,
|
|
11
|
+
'Content-Type': 'application/json',
|
|
12
|
+
},
|
|
13
|
+
});
|
|
14
|
+
if (!response.ok) {
|
|
15
|
+
const body = await response.text();
|
|
16
|
+
throw new Error(`Auth failed (${response.status}): ${body}`);
|
|
17
|
+
}
|
|
18
|
+
return response.json();
|
|
19
|
+
}
|
|
20
|
+
export async function initAuth(apiUrl, token) {
|
|
21
|
+
logger.info('Authenticating with TeamVibe API...');
|
|
22
|
+
const auth = await fetchCredentials(apiUrl, token);
|
|
23
|
+
currentCredentials = auth.credentials;
|
|
24
|
+
currentConfig = auth.config;
|
|
25
|
+
logger.info(`Authenticated successfully. Region: ${auth.config.region}`);
|
|
26
|
+
logger.info(` SQS Queue: ${auth.config.sqsQueueUrl}`);
|
|
27
|
+
logger.info(` Sessions Table: ${auth.config.sessionsTable}`);
|
|
28
|
+
// Schedule credential refresh
|
|
29
|
+
refreshTimer = setInterval(async () => {
|
|
30
|
+
try {
|
|
31
|
+
logger.info('Refreshing credentials...');
|
|
32
|
+
const refreshed = await fetchCredentials(apiUrl, token);
|
|
33
|
+
currentCredentials = refreshed.credentials;
|
|
34
|
+
currentConfig = refreshed.config;
|
|
35
|
+
logger.info('Credentials refreshed successfully');
|
|
36
|
+
}
|
|
37
|
+
catch (error) {
|
|
38
|
+
logger.error(`Credential refresh failed: ${error instanceof Error ? error.message : error}`);
|
|
39
|
+
}
|
|
40
|
+
}, REFRESH_INTERVAL_MS);
|
|
41
|
+
}
|
|
42
|
+
export function getCredentials() {
|
|
43
|
+
if (!currentCredentials) {
|
|
44
|
+
throw new Error('Auth not initialized. Call initAuth() first.');
|
|
45
|
+
}
|
|
46
|
+
return currentCredentials;
|
|
47
|
+
}
|
|
48
|
+
export function getAuthConfig() {
|
|
49
|
+
if (!currentConfig) {
|
|
50
|
+
throw new Error('Auth not initialized. Call initAuth() first.');
|
|
51
|
+
}
|
|
52
|
+
return currentConfig;
|
|
53
|
+
}
|
|
54
|
+
export function isTokenMode() {
|
|
55
|
+
return currentCredentials !== null;
|
|
56
|
+
}
|
|
57
|
+
export function stopRefresh() {
|
|
58
|
+
if (refreshTimer) {
|
|
59
|
+
clearInterval(refreshTimer);
|
|
60
|
+
refreshTimer = null;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TeamVibeQueueMessage } from './types.js';
|
|
2
|
+
import { type SessionLogger } from './logger.js';
|
|
3
|
+
export interface SpawnResult {
|
|
4
|
+
success: boolean;
|
|
5
|
+
output: string;
|
|
6
|
+
exitCode: number | null;
|
|
7
|
+
error?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string): Promise<SpawnResult>;
|
|
10
|
+
export declare function getActiveProcessCount(): number;
|
|
11
|
+
export declare function isAtCapacity(): boolean;
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { join, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { WebClient } from '@slack/web-api';
|
|
5
|
+
import { config } from './config.js';
|
|
6
|
+
import { logger } from './logger.js';
|
|
7
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
const SLACK_TOOL_PATH = join(__dirname, 'scripts/slack-tool.ts');
|
|
9
|
+
// Per-token Slack client cache for thread context
|
|
10
|
+
const slackClients = new Map();
|
|
11
|
+
function getSlackClient(botToken) {
|
|
12
|
+
let client = slackClients.get(botToken);
|
|
13
|
+
if (!client) {
|
|
14
|
+
client = new WebClient(botToken);
|
|
15
|
+
slackClients.set(botToken, client);
|
|
16
|
+
}
|
|
17
|
+
return client;
|
|
18
|
+
}
|
|
19
|
+
async function countNewThreadMessages(botToken, channel, threadTs, sinceTs) {
|
|
20
|
+
try {
|
|
21
|
+
const slack = getSlackClient(botToken);
|
|
22
|
+
const result = await slack.conversations.replies({
|
|
23
|
+
channel,
|
|
24
|
+
ts: threadTs,
|
|
25
|
+
limit: 100,
|
|
26
|
+
});
|
|
27
|
+
if (!result.messages)
|
|
28
|
+
return 0;
|
|
29
|
+
const newMessages = result.messages.filter((msg) => {
|
|
30
|
+
if (!msg.ts)
|
|
31
|
+
return false;
|
|
32
|
+
if (parseFloat(msg.ts) <= parseFloat(sinceTs))
|
|
33
|
+
return false;
|
|
34
|
+
if (msg.bot_id)
|
|
35
|
+
return false;
|
|
36
|
+
return true;
|
|
37
|
+
});
|
|
38
|
+
return newMessages.length;
|
|
39
|
+
}
|
|
40
|
+
catch (error) {
|
|
41
|
+
logger.warn(`Failed to count new thread messages: ${error}`);
|
|
42
|
+
return 0;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
function buildSystemPrompt(persona) {
|
|
46
|
+
let prompt = persona?.systemPrompt || getDefaultSystemPrompt();
|
|
47
|
+
// Append standard communication instructions
|
|
48
|
+
prompt += `\n\n## Communication
|
|
49
|
+
You have Slack tools available via Bash. Use these commands to communicate:
|
|
50
|
+
|
|
51
|
+
**Send a message** (REQUIRED - always use this to respond):
|
|
52
|
+
\`\`\`bash
|
|
53
|
+
npx tsx $SLACK_TOOL_PATH send_message "Your message here"
|
|
54
|
+
\`\`\`
|
|
55
|
+
|
|
56
|
+
**Add emoji reaction:**
|
|
57
|
+
\`\`\`bash
|
|
58
|
+
npx tsx $SLACK_TOOL_PATH add_reaction emoji_name
|
|
59
|
+
\`\`\`
|
|
60
|
+
|
|
61
|
+
**Remove emoji reaction:**
|
|
62
|
+
\`\`\`bash
|
|
63
|
+
npx tsx $SLACK_TOOL_PATH remove_reaction emoji_name
|
|
64
|
+
\`\`\`
|
|
65
|
+
|
|
66
|
+
**Read thread history:**
|
|
67
|
+
\`\`\`bash
|
|
68
|
+
npx tsx $SLACK_TOOL_PATH read_thread [limit]
|
|
69
|
+
\`\`\`
|
|
70
|
+
|
|
71
|
+
**Upload code/text snippet:**
|
|
72
|
+
\`\`\`bash
|
|
73
|
+
npx tsx $SLACK_TOOL_PATH upload_snippet "title" "content" [filetype]
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
**Important:** To respond to the user, you MUST use the send_message command via Bash - plain text output will NOT be seen by the user. Always send at least one message back.
|
|
77
|
+
All commands return JSON: {"ok": true, ...} on success or {"ok": false, "error": "..."} on failure.
|
|
78
|
+
|
|
79
|
+
## Response Guidelines
|
|
80
|
+
1. **Always respond** - Use send_message to reply
|
|
81
|
+
2. **Acknowledge long tasks** - Send a quick message first for time-consuming work
|
|
82
|
+
3. **Be concise** - Use bullet points, keep it focused
|
|
83
|
+
4. **Use Slack formatting** - \`code\`, *bold*, _italic_, \\\`\\\`\\\`code blocks\\\`\\\`\\\`, > quotes
|
|
84
|
+
5. **For long code** - Use upload_snippet instead of huge code blocks`;
|
|
85
|
+
return prompt;
|
|
86
|
+
}
|
|
87
|
+
function getDefaultSystemPrompt() {
|
|
88
|
+
return `You are a helpful assistant operating in a team's Slack workspace. Team members contact you via Slack messages.
|
|
89
|
+
|
|
90
|
+
## Your Setup
|
|
91
|
+
You have access to the company's **knowledge base** (the current working directory). You should:
|
|
92
|
+
- Read and search files to understand available tools and information
|
|
93
|
+
- Execute available commands, skills, and automation scripts
|
|
94
|
+
- Run shell commands for tasks (API calls, data processing, etc.)
|
|
95
|
+
- Create temporary local files when needed for your work
|
|
96
|
+
- Do NOT create new permanent files or edit existing files unless explicitly asked`;
|
|
97
|
+
}
|
|
98
|
+
function buildPrompt(msg, systemPrompt, resumeHint) {
|
|
99
|
+
let prompt = systemPrompt;
|
|
100
|
+
prompt += '\n\n---\n\n## Incoming Slack Message\n\n';
|
|
101
|
+
prompt += `**From:** ${msg.sender.name}`;
|
|
102
|
+
if (msg.sender.id && msg.sender.id !== msg.sender.name) {
|
|
103
|
+
prompt += ` (${msg.sender.id})`;
|
|
104
|
+
}
|
|
105
|
+
prompt += '\n';
|
|
106
|
+
if (msg.response_context.slack) {
|
|
107
|
+
const slack = msg.response_context.slack;
|
|
108
|
+
const channelType = slack.channel.startsWith('D')
|
|
109
|
+
? 'Direct Message'
|
|
110
|
+
: `Channel ${slack.channel}`;
|
|
111
|
+
prompt += `**Channel:** ${channelType}\n`;
|
|
112
|
+
}
|
|
113
|
+
prompt += `\n**Message:**\n${msg.text}`;
|
|
114
|
+
if (msg.attachments.length > 0) {
|
|
115
|
+
prompt += '\n\n**Attachments:**\n';
|
|
116
|
+
msg.attachments.forEach((att) => {
|
|
117
|
+
prompt += `- ${att.filename} (${att.mimetype}): ${att.url}\n`;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (msg.type === 'approval_response' && msg.approval) {
|
|
121
|
+
prompt += `\n\n**Action Response:** User ${msg.approval.approved ? 'APPROVED' : 'REJECTED'} the pending action: ${msg.approval.action_id}`;
|
|
122
|
+
}
|
|
123
|
+
if (resumeHint) {
|
|
124
|
+
prompt += `\n\n${resumeHint}`;
|
|
125
|
+
}
|
|
126
|
+
prompt += '\n\n---\n\nNow respond to this user using the send_message command via Bash.';
|
|
127
|
+
return prompt;
|
|
128
|
+
}
|
|
129
|
+
function truncateOutput(output, maxLen = 200) {
|
|
130
|
+
const outputStr = typeof output === 'string' ? output : JSON.stringify(output);
|
|
131
|
+
if (outputStr.includes('"type":"base64"') ||
|
|
132
|
+
outputStr.includes('base64,') ||
|
|
133
|
+
(outputStr.length > 500 && (outputStr.match(/[A-Za-z0-9+/=]/g)?.length || 0) / outputStr.length > 0.9)) {
|
|
134
|
+
return '[binary/base64 data truncated]';
|
|
135
|
+
}
|
|
136
|
+
else if (outputStr.length > maxLen) {
|
|
137
|
+
return outputStr.slice(0, maxLen) + '...';
|
|
138
|
+
}
|
|
139
|
+
return outputStr;
|
|
140
|
+
}
|
|
141
|
+
function formatStreamEvent(event) {
|
|
142
|
+
switch (event.type) {
|
|
143
|
+
case 'system':
|
|
144
|
+
return `[system] ${event.message || JSON.stringify(event)}`;
|
|
145
|
+
case 'user':
|
|
146
|
+
if (event.message?.content && Array.isArray(event.message.content)) {
|
|
147
|
+
const parts = event.message.content
|
|
148
|
+
.map((c) => {
|
|
149
|
+
if (c.type === 'tool_result') {
|
|
150
|
+
return `[tool_result] ${truncateOutput(c.content)}`;
|
|
151
|
+
}
|
|
152
|
+
return null;
|
|
153
|
+
})
|
|
154
|
+
.filter(Boolean);
|
|
155
|
+
return parts.length > 0 ? `[user] ${parts.join(' ')}` : null;
|
|
156
|
+
}
|
|
157
|
+
return null;
|
|
158
|
+
case 'assistant':
|
|
159
|
+
if (event.message?.content) {
|
|
160
|
+
const parts = event.message.content
|
|
161
|
+
.map((c) => {
|
|
162
|
+
if (c.type === 'text')
|
|
163
|
+
return c.text;
|
|
164
|
+
if (c.type === 'tool_use')
|
|
165
|
+
return `[tool_call] ${c.name}`;
|
|
166
|
+
return null;
|
|
167
|
+
})
|
|
168
|
+
.filter(Boolean);
|
|
169
|
+
return parts.length > 0 ? `[assistant] ${parts.join(' ')}` : null;
|
|
170
|
+
}
|
|
171
|
+
return null;
|
|
172
|
+
case 'tool_use': {
|
|
173
|
+
const input = JSON.stringify(event.input || {});
|
|
174
|
+
const truncatedInput = input.length > 200 ? input.slice(0, 200) + '...' : input;
|
|
175
|
+
return `[tool_use] ${event.name}(${truncatedInput})`;
|
|
176
|
+
}
|
|
177
|
+
case 'tool_result':
|
|
178
|
+
return `[tool_result] ${truncateOutput(event.content || event.output || '')}`;
|
|
179
|
+
case 'result':
|
|
180
|
+
return `[result] Cost: $${event.cost_usd?.toFixed(4) || '?'}, Duration: ${event.duration_ms || '?'}ms`;
|
|
181
|
+
default:
|
|
182
|
+
return null;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs) {
|
|
186
|
+
const slackContext = msg.response_context.slack;
|
|
187
|
+
const isCronMessage = msg.source === 'cron';
|
|
188
|
+
if (!slackContext && !isCronMessage) {
|
|
189
|
+
throw new Error('No Slack context in message');
|
|
190
|
+
}
|
|
191
|
+
// Build resume hint
|
|
192
|
+
let resumeHint;
|
|
193
|
+
if (!isFirstMessage && lastMessageTs && slackContext?.channel && slackContext?.thread_ts) {
|
|
194
|
+
const newMessageCount = await countNewThreadMessages(msg.teamvibe.botToken, slackContext.channel, slackContext.thread_ts, lastMessageTs);
|
|
195
|
+
if (newMessageCount > 0) {
|
|
196
|
+
resumeHint = `**Resumed session:** ${newMessageCount} new message(s) in the thread since your last response. Consider using \`read_thread\` to see what was discussed before responding.`;
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
resumeHint = `**Resumed session:** No new messages since your last response.`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return new Promise((resolve) => {
|
|
203
|
+
const systemPrompt = buildSystemPrompt(msg.teamvibe.persona);
|
|
204
|
+
const prompt = buildPrompt(msg, systemPrompt, resumeHint);
|
|
205
|
+
const args = [
|
|
206
|
+
'--print',
|
|
207
|
+
'--verbose',
|
|
208
|
+
'--output-format',
|
|
209
|
+
'stream-json',
|
|
210
|
+
'--max-turns',
|
|
211
|
+
'50',
|
|
212
|
+
'--allowedTools',
|
|
213
|
+
'Bash,Read,WebFetch',
|
|
214
|
+
];
|
|
215
|
+
// Handle session continuity
|
|
216
|
+
if (sessionId) {
|
|
217
|
+
if (isFirstMessage) {
|
|
218
|
+
args.push('--session-id', sessionId);
|
|
219
|
+
}
|
|
220
|
+
else {
|
|
221
|
+
args.push('--resume', sessionId);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
sessionLog.info(`Spawning Claude Code with args: ${JSON.stringify(args)}`);
|
|
225
|
+
sessionLog.info(`Working directory: ${cwd}`);
|
|
226
|
+
sessionLog.info(`Prompt length: ${prompt.length} chars`);
|
|
227
|
+
sessionLog.logPrompt(prompt);
|
|
228
|
+
const proc = spawn(config.CLAUDE_CLI_PATH, args, {
|
|
229
|
+
cwd,
|
|
230
|
+
env: {
|
|
231
|
+
...process.env,
|
|
232
|
+
CI: 'true',
|
|
233
|
+
SLACK_BOT_TOKEN: msg.teamvibe.botToken,
|
|
234
|
+
SLACK_TOOL_PATH,
|
|
235
|
+
SLACK_FILES_DIR: join(cwd, '.local/slack-files'),
|
|
236
|
+
...(slackContext && {
|
|
237
|
+
SLACK_CHANNEL: slackContext.channel,
|
|
238
|
+
SLACK_THREAD_TS: slackContext.thread_ts,
|
|
239
|
+
SLACK_MESSAGE_TS: slackContext.message_ts,
|
|
240
|
+
}),
|
|
241
|
+
},
|
|
242
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
243
|
+
});
|
|
244
|
+
// Track active process
|
|
245
|
+
activeProcesses.set(msg.id, proc);
|
|
246
|
+
proc.stdin?.write(prompt);
|
|
247
|
+
proc.stdin?.end();
|
|
248
|
+
let stdout = '';
|
|
249
|
+
let stderr = '';
|
|
250
|
+
let stdoutBuffer = '';
|
|
251
|
+
proc.stdout.on('data', (data) => {
|
|
252
|
+
const chunk = data.toString();
|
|
253
|
+
stdout += chunk;
|
|
254
|
+
stdoutBuffer += chunk;
|
|
255
|
+
const lastNewline = stdoutBuffer.lastIndexOf('\n');
|
|
256
|
+
if (lastNewline === -1)
|
|
257
|
+
return;
|
|
258
|
+
const completeData = stdoutBuffer.slice(0, lastNewline);
|
|
259
|
+
stdoutBuffer = stdoutBuffer.slice(lastNewline + 1);
|
|
260
|
+
const lines = completeData.split('\n').filter((line) => line.trim());
|
|
261
|
+
for (const line of lines) {
|
|
262
|
+
try {
|
|
263
|
+
const event = JSON.parse(line);
|
|
264
|
+
const formatted = formatStreamEvent(event);
|
|
265
|
+
if (formatted) {
|
|
266
|
+
sessionLog.claude('stdout', formatted);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
catch {
|
|
270
|
+
const truncated = line.length > 500 ? line.slice(0, 500) + '...' : line;
|
|
271
|
+
sessionLog.claude('stdout', truncated.trimEnd());
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
});
|
|
275
|
+
proc.stderr.on('data', (data) => {
|
|
276
|
+
const chunk = data.toString();
|
|
277
|
+
stderr += chunk;
|
|
278
|
+
sessionLog.claude('stderr', chunk.trimEnd());
|
|
279
|
+
});
|
|
280
|
+
const timeout = setTimeout(() => {
|
|
281
|
+
logger.error(`Claude Code process timed out after ${config.CLAUDE_TIMEOUT_MS}ms`);
|
|
282
|
+
proc.kill('SIGTERM');
|
|
283
|
+
setTimeout(() => proc.kill('SIGKILL'), 5000);
|
|
284
|
+
}, config.CLAUDE_TIMEOUT_MS);
|
|
285
|
+
proc.on('close', (code) => {
|
|
286
|
+
clearTimeout(timeout);
|
|
287
|
+
activeProcesses.delete(msg.id);
|
|
288
|
+
const result = {
|
|
289
|
+
success: code === 0,
|
|
290
|
+
output: stdout,
|
|
291
|
+
exitCode: code,
|
|
292
|
+
};
|
|
293
|
+
if (code !== 0) {
|
|
294
|
+
result.error = stderr || `Process exited with code ${code}`;
|
|
295
|
+
}
|
|
296
|
+
resolve(result);
|
|
297
|
+
});
|
|
298
|
+
proc.on('error', (error) => {
|
|
299
|
+
clearTimeout(timeout);
|
|
300
|
+
activeProcesses.delete(msg.id);
|
|
301
|
+
resolve({
|
|
302
|
+
success: false,
|
|
303
|
+
output: stdout,
|
|
304
|
+
exitCode: null,
|
|
305
|
+
error: error.message,
|
|
306
|
+
});
|
|
307
|
+
});
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
const activeProcesses = new Map();
|
|
311
|
+
export function getActiveProcessCount() {
|
|
312
|
+
return activeProcesses.size;
|
|
313
|
+
}
|
|
314
|
+
export function isAtCapacity() {
|
|
315
|
+
return activeProcesses.size >= config.MAX_CONCURRENT_SESSIONS;
|
|
316
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
declare const configSchema: z.ZodObject<{
|
|
3
|
+
AWS_REGION: z.ZodDefault<z.ZodString>;
|
|
4
|
+
SQS_QUEUE_URL: z.ZodOptional<z.ZodString>;
|
|
5
|
+
SESSIONS_TABLE: z.ZodOptional<z.ZodString>;
|
|
6
|
+
TEAMVIBE_API_URL: z.ZodOptional<z.ZodString>;
|
|
7
|
+
TEAMVIBE_POLLER_TOKEN: z.ZodOptional<z.ZodString>;
|
|
8
|
+
MAX_CONCURRENT_SESSIONS: z.ZodDefault<z.ZodNumber>;
|
|
9
|
+
POLL_WAIT_TIME_SECONDS: z.ZodDefault<z.ZodNumber>;
|
|
10
|
+
VISIBILITY_TIMEOUT_SECONDS: z.ZodDefault<z.ZodNumber>;
|
|
11
|
+
HEARTBEAT_INTERVAL_MS: z.ZodDefault<z.ZodNumber>;
|
|
12
|
+
CLAUDE_TIMEOUT_MS: z.ZodDefault<z.ZodNumber>;
|
|
13
|
+
STALE_LOCK_TIMEOUT_MS: z.ZodDefault<z.ZodNumber>;
|
|
14
|
+
KB_BASE_PATH: z.ZodDefault<z.ZodString>;
|
|
15
|
+
DEFAULT_KB_PATH: z.ZodDefault<z.ZodString>;
|
|
16
|
+
CLAUDE_CLI_PATH: z.ZodDefault<z.ZodString>;
|
|
17
|
+
KB_AUTO_UPDATE: z.ZodEffects<z.ZodDefault<z.ZodString>, boolean, string | undefined>;
|
|
18
|
+
KB_UPDATE_COOLDOWN_MS: z.ZodDefault<z.ZodNumber>;
|
|
19
|
+
}, "strip", z.ZodTypeAny, {
|
|
20
|
+
AWS_REGION: string;
|
|
21
|
+
MAX_CONCURRENT_SESSIONS: number;
|
|
22
|
+
POLL_WAIT_TIME_SECONDS: number;
|
|
23
|
+
VISIBILITY_TIMEOUT_SECONDS: number;
|
|
24
|
+
HEARTBEAT_INTERVAL_MS: number;
|
|
25
|
+
CLAUDE_TIMEOUT_MS: number;
|
|
26
|
+
STALE_LOCK_TIMEOUT_MS: number;
|
|
27
|
+
KB_BASE_PATH: string;
|
|
28
|
+
DEFAULT_KB_PATH: string;
|
|
29
|
+
CLAUDE_CLI_PATH: string;
|
|
30
|
+
KB_AUTO_UPDATE: boolean;
|
|
31
|
+
KB_UPDATE_COOLDOWN_MS: number;
|
|
32
|
+
SQS_QUEUE_URL?: string | undefined;
|
|
33
|
+
SESSIONS_TABLE?: string | undefined;
|
|
34
|
+
TEAMVIBE_API_URL?: string | undefined;
|
|
35
|
+
TEAMVIBE_POLLER_TOKEN?: string | undefined;
|
|
36
|
+
}, {
|
|
37
|
+
AWS_REGION?: string | undefined;
|
|
38
|
+
SQS_QUEUE_URL?: string | undefined;
|
|
39
|
+
SESSIONS_TABLE?: string | undefined;
|
|
40
|
+
TEAMVIBE_API_URL?: string | undefined;
|
|
41
|
+
TEAMVIBE_POLLER_TOKEN?: string | undefined;
|
|
42
|
+
MAX_CONCURRENT_SESSIONS?: number | undefined;
|
|
43
|
+
POLL_WAIT_TIME_SECONDS?: number | undefined;
|
|
44
|
+
VISIBILITY_TIMEOUT_SECONDS?: number | undefined;
|
|
45
|
+
HEARTBEAT_INTERVAL_MS?: number | undefined;
|
|
46
|
+
CLAUDE_TIMEOUT_MS?: number | undefined;
|
|
47
|
+
STALE_LOCK_TIMEOUT_MS?: number | undefined;
|
|
48
|
+
KB_BASE_PATH?: string | undefined;
|
|
49
|
+
DEFAULT_KB_PATH?: string | undefined;
|
|
50
|
+
CLAUDE_CLI_PATH?: string | undefined;
|
|
51
|
+
KB_AUTO_UPDATE?: string | undefined;
|
|
52
|
+
KB_UPDATE_COOLDOWN_MS?: number | undefined;
|
|
53
|
+
}>;
|
|
54
|
+
export type Config = z.infer<typeof configSchema>;
|
|
55
|
+
export declare function loadConfig(): Config;
|
|
56
|
+
export declare const config: {
|
|
57
|
+
AWS_REGION: string;
|
|
58
|
+
MAX_CONCURRENT_SESSIONS: number;
|
|
59
|
+
POLL_WAIT_TIME_SECONDS: number;
|
|
60
|
+
VISIBILITY_TIMEOUT_SECONDS: number;
|
|
61
|
+
HEARTBEAT_INTERVAL_MS: number;
|
|
62
|
+
CLAUDE_TIMEOUT_MS: number;
|
|
63
|
+
STALE_LOCK_TIMEOUT_MS: number;
|
|
64
|
+
KB_BASE_PATH: string;
|
|
65
|
+
DEFAULT_KB_PATH: string;
|
|
66
|
+
CLAUDE_CLI_PATH: string;
|
|
67
|
+
KB_AUTO_UPDATE: boolean;
|
|
68
|
+
KB_UPDATE_COOLDOWN_MS: number;
|
|
69
|
+
SQS_QUEUE_URL?: string | undefined;
|
|
70
|
+
SESSIONS_TABLE?: string | undefined;
|
|
71
|
+
TEAMVIBE_API_URL?: string | undefined;
|
|
72
|
+
TEAMVIBE_POLLER_TOKEN?: string | undefined;
|
|
73
|
+
};
|
|
74
|
+
export {};
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { config as dotenvConfig } from 'dotenv';
|
|
3
|
+
dotenvConfig();
|
|
4
|
+
const configSchema = z.object({
|
|
5
|
+
// AWS
|
|
6
|
+
AWS_REGION: z.string().default('eu-central-1'),
|
|
7
|
+
SQS_QUEUE_URL: z.string().optional(),
|
|
8
|
+
SESSIONS_TABLE: z.string().optional(),
|
|
9
|
+
// Token-based auth (alternative to direct AWS credentials)
|
|
10
|
+
TEAMVIBE_API_URL: z.string().optional(),
|
|
11
|
+
TEAMVIBE_POLLER_TOKEN: z.string().optional(),
|
|
12
|
+
// Poller settings
|
|
13
|
+
MAX_CONCURRENT_SESSIONS: z.coerce.number().default(5),
|
|
14
|
+
POLL_WAIT_TIME_SECONDS: z.coerce.number().default(20),
|
|
15
|
+
VISIBILITY_TIMEOUT_SECONDS: z.coerce.number().default(300),
|
|
16
|
+
HEARTBEAT_INTERVAL_MS: z.coerce.number().default(60000), // 1 minute
|
|
17
|
+
CLAUDE_TIMEOUT_MS: z.coerce.number().default(1800000), // 30 minutes
|
|
18
|
+
STALE_LOCK_TIMEOUT_MS: z.coerce.number().default(2100000), // 35 minutes
|
|
19
|
+
// Paths
|
|
20
|
+
KB_BASE_PATH: z.string().default(`${process.env['HOME']}/.teamvibe/knowledge-bases`),
|
|
21
|
+
DEFAULT_KB_PATH: z.string().default(`${process.env['HOME']}/.teamvibe/default-kb`),
|
|
22
|
+
CLAUDE_CLI_PATH: z.string().default('claude'),
|
|
23
|
+
// Knowledge base auto-update
|
|
24
|
+
KB_AUTO_UPDATE: z
|
|
25
|
+
.string()
|
|
26
|
+
.default('true')
|
|
27
|
+
.transform((v) => v.toLowerCase() === 'true'),
|
|
28
|
+
KB_UPDATE_COOLDOWN_MS: z.coerce.number().default(300000), // 5 minutes
|
|
29
|
+
});
|
|
30
|
+
export function loadConfig() {
|
|
31
|
+
const result = configSchema.safeParse(process.env);
|
|
32
|
+
if (!result.success) {
|
|
33
|
+
console.error('Configuration error:');
|
|
34
|
+
result.error.issues.forEach((issue) => {
|
|
35
|
+
console.error(` ${issue.path.join('.')}: ${issue.message}`);
|
|
36
|
+
});
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
39
|
+
const data = result.data;
|
|
40
|
+
const isTokenMode = !!(data.TEAMVIBE_API_URL && data.TEAMVIBE_POLLER_TOKEN);
|
|
41
|
+
const isDirectMode = !!(data.SQS_QUEUE_URL && data.SESSIONS_TABLE);
|
|
42
|
+
if (!isTokenMode && !isDirectMode) {
|
|
43
|
+
console.error('Configuration error: Provide either TEAMVIBE_API_URL + TEAMVIBE_POLLER_TOKEN (token mode) or SQS_QUEUE_URL + SESSIONS_TABLE (direct mode)');
|
|
44
|
+
process.exit(1);
|
|
45
|
+
}
|
|
46
|
+
return data;
|
|
47
|
+
}
|
|
48
|
+
export const config = loadConfig();
|
package/dist/index.d.ts
ADDED