@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/index.js ADDED
@@ -0,0 +1,238 @@
1
+ #!/usr/bin/env node
2
+ import { config } from './config.js';
3
+ import { logger } from './logger.js';
4
+ import { pollMessages, deleteMessage, extendVisibility, } from './sqs-poller.js';
5
+ import { spawnClaudeCode, isAtCapacity, getActiveProcessCount } from './claude-spawner.js';
6
+ import { sendSlackError, addReaction, getUserInfo, startTypingIndicator } from './slack-client.js';
7
+ import { acquireSessionLock, releaseSessionLock } from './session-store.js';
8
+ import { getKnowledgeBasePath, ensureDirectories } from './kb-manager.js';
9
+ import { initAuth, stopRefresh } from './auth-provider.js';
10
+ logger.info('TeamVibe Poller starting...');
11
+ logger.info(` Max concurrent: ${config.MAX_CONCURRENT_SESSIONS}`);
12
+ logger.info(` KB base path: ${config.KB_BASE_PATH}`);
13
+ // Track active message processing
14
+ const processingMessages = new Set();
15
+ // Per-thread completion signals
16
+ const threadCompletionSignals = new Map();
17
+ const waitingCountByThread = new Map();
18
+ function getQueueStats() {
19
+ let totalWaiting = 0;
20
+ waitingCountByThread.forEach((count) => {
21
+ totalWaiting += count;
22
+ });
23
+ return {
24
+ processing: processingMessages.size,
25
+ threadsWithWaiting: waitingCountByThread.size,
26
+ totalWaiting,
27
+ };
28
+ }
29
+ function logQueueState(context) {
30
+ const stats = getQueueStats();
31
+ logger.info(`[Queue] ${context} | Processing: ${stats.processing}, Threads with waiting: ${stats.threadsWithWaiting}, Total waiting: ${stats.totalWaiting}`);
32
+ }
33
+ function waitForThreadCompletion(threadId) {
34
+ return new Promise((resolve) => {
35
+ const signals = threadCompletionSignals.get(threadId) || [];
36
+ signals.push(resolve);
37
+ threadCompletionSignals.set(threadId, signals);
38
+ const currentCount = waitingCountByThread.get(threadId) || 0;
39
+ waitingCountByThread.set(threadId, currentCount + 1);
40
+ logger.info(`[Queue] Thread ${threadId}: ${currentCount + 1} message(s) now waiting`);
41
+ logQueueState('After enqueue');
42
+ });
43
+ }
44
+ function signalThreadCompletion(threadId) {
45
+ const signals = threadCompletionSignals.get(threadId);
46
+ const waitingCount = signals?.length || 0;
47
+ if (signals && signals.length > 0) {
48
+ logger.info(`[Queue] Thread ${threadId} completed, waking ${waitingCount} waiting message(s)`);
49
+ signals.forEach((resolve) => resolve());
50
+ threadCompletionSignals.delete(threadId);
51
+ waitingCountByThread.delete(threadId);
52
+ logQueueState('After signal');
53
+ }
54
+ }
55
+ function startHeartbeat(receiptHandle, sessionLog) {
56
+ return setInterval(async () => {
57
+ try {
58
+ await extendVisibility(receiptHandle, config.VISIBILITY_TIMEOUT_SECONDS);
59
+ sessionLog.info(`Heartbeat: extended visibility by ${config.VISIBILITY_TIMEOUT_SECONDS}s`);
60
+ }
61
+ catch (error) {
62
+ sessionLog.error(`Heartbeat failed: ${error instanceof Error ? error.message : error}`);
63
+ }
64
+ }, config.HEARTBEAT_INTERVAL_MS);
65
+ }
66
+ async function processMessage(received) {
67
+ const { queueMessage, receiptHandle, messageId } = received;
68
+ const logSessionId = messageId.slice(0, 8);
69
+ const threadId = queueMessage.thread_id;
70
+ const sessionLog = logger.createSession(logSessionId);
71
+ // Fetch user info if we only have ID
72
+ if (queueMessage.source !== 'cron' &&
73
+ queueMessage.sender.name === 'Unknown User' &&
74
+ queueMessage.sender.id !== 'unknown') {
75
+ const userInfo = await getUserInfo(queueMessage.teamvibe.botToken, queueMessage.sender.id);
76
+ queueMessage.sender.name = userInfo.realName;
77
+ sessionLog.info(`Resolved user: ${userInfo.realName} (@${userInfo.name})`);
78
+ }
79
+ sessionLog.info(`Processing message from ${queueMessage.sender.name} (${queueMessage.sender.id})`);
80
+ sessionLog.info(`Thread: ${threadId}`);
81
+ sessionLog.info(`Persona: ${queueMessage.teamvibe.persona?.name ?? 'none'}`);
82
+ sessionLog.info(`Type: ${queueMessage.type}`);
83
+ sessionLog.info(`Log file: ${sessionLog.getLogFile()}`);
84
+ // Get knowledge base path for this persona
85
+ const kbPath = await getKnowledgeBasePath(queueMessage.teamvibe.persona?.knowledgeBase);
86
+ // Try to acquire session lock
87
+ let lockResult = await acquireSessionLock(threadId, kbPath);
88
+ if (!lockResult.success) {
89
+ sessionLog.info(`Session ${threadId} is processing, waiting for completion...`);
90
+ const waitHeartbeat = startHeartbeat(receiptHandle, sessionLog);
91
+ try {
92
+ await waitForThreadCompletion(threadId);
93
+ sessionLog.info(`Thread ${threadId} completed, retrying lock acquisition`);
94
+ lockResult = await acquireSessionLock(threadId, kbPath);
95
+ if (!lockResult.success) {
96
+ sessionLog.info('Lock still held, re-queuing for next completion');
97
+ clearInterval(waitHeartbeat);
98
+ return processMessage(received);
99
+ }
100
+ }
101
+ finally {
102
+ clearInterval(waitHeartbeat);
103
+ }
104
+ }
105
+ const { session, lockToken } = lockResult;
106
+ const isFirstMessage = session.message_count === 1;
107
+ sessionLog.info(`Acquired lock on session ${threadId}, Claude session: ${session.session_id}, mode: ${isFirstMessage ? 'new' : 'resume'}, message #${session.message_count}`);
108
+ logQueueState(`Lock acquired for ${threadId}`);
109
+ processingMessages.add(messageId);
110
+ const heartbeat = startHeartbeat(receiptHandle, sessionLog);
111
+ const hasSlackContext = Boolean(queueMessage.response_context.slack?.channel &&
112
+ queueMessage.response_context.slack?.message_ts);
113
+ // Start typing indicator
114
+ const stopTyping = hasSlackContext
115
+ ? startTypingIndicator(queueMessage)
116
+ : undefined;
117
+ try {
118
+ const result = await spawnClaudeCode(queueMessage, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts);
119
+ stopTyping?.();
120
+ if (result.success) {
121
+ sessionLog.info('Claude Code completed successfully');
122
+ if (lockToken) {
123
+ const lastMessageTs = queueMessage.response_context.slack?.message_ts;
124
+ await releaseSessionLock(threadId, lockToken, 'idle', lastMessageTs);
125
+ }
126
+ }
127
+ else {
128
+ sessionLog.error(`Claude Code failed: ${result.error}`);
129
+ if (hasSlackContext) {
130
+ await sendSlackError(queueMessage, result.error || `Process exited with code ${result.exitCode}`);
131
+ await addReaction(queueMessage, 'x');
132
+ }
133
+ if (lockToken) {
134
+ await releaseSessionLock(threadId, lockToken, 'idle');
135
+ }
136
+ }
137
+ await deleteMessage(receiptHandle);
138
+ sessionLog.info('Message processed and deleted');
139
+ }
140
+ catch (error) {
141
+ stopTyping?.();
142
+ sessionLog.error(`Error processing message: ${error instanceof Error ? error.message : error}`);
143
+ if (lockToken) {
144
+ try {
145
+ await releaseSessionLock(threadId, lockToken, 'idle');
146
+ }
147
+ catch (releaseError) {
148
+ sessionLog.error(`Failed to release lock: ${releaseError}`);
149
+ }
150
+ }
151
+ if (hasSlackContext) {
152
+ try {
153
+ await sendSlackError(queueMessage, error instanceof Error ? error.message : 'Unknown error');
154
+ await addReaction(queueMessage, 'x');
155
+ }
156
+ catch (slackError) {
157
+ sessionLog.error(`Failed to send Slack error: ${slackError}`);
158
+ }
159
+ }
160
+ throw error;
161
+ }
162
+ finally {
163
+ clearInterval(heartbeat);
164
+ processingMessages.delete(messageId);
165
+ signalThreadCompletion(threadId);
166
+ }
167
+ }
168
+ async function pollLoop() {
169
+ logger.info('Poll loop started');
170
+ while (true) {
171
+ try {
172
+ if (isAtCapacity()) {
173
+ logger.debug(`At capacity (${getActiveProcessCount()}/${config.MAX_CONCURRENT_SESSIONS}), waiting...`);
174
+ await sleep(1000);
175
+ continue;
176
+ }
177
+ const availableSlots = config.MAX_CONCURRENT_SESSIONS - getActiveProcessCount();
178
+ logger.debug(`Polling for up to ${availableSlots} messages...`);
179
+ const messages = await pollMessages(availableSlots);
180
+ if (messages.length === 0) {
181
+ continue;
182
+ }
183
+ logger.info(`Received ${messages.length} message(s) from SQS`);
184
+ logQueueState('After SQS poll');
185
+ const processPromises = messages.map((msg) => processMessage(msg).catch((error) => {
186
+ logger.error(`Failed to process message ${msg.messageId}:`, error);
187
+ }));
188
+ // Don't await - let them run in parallel
189
+ Promise.all(processPromises);
190
+ }
191
+ catch (error) {
192
+ logger.error('Error in poll loop:', error);
193
+ await sleep(5000);
194
+ }
195
+ }
196
+ }
197
+ function sleep(ms) {
198
+ return new Promise((resolve) => setTimeout(resolve, ms));
199
+ }
200
+ // Graceful shutdown
201
+ let shuttingDown = false;
202
+ async function shutdown(signal) {
203
+ if (shuttingDown)
204
+ return;
205
+ shuttingDown = true;
206
+ logger.info(`${signal} received, shutting down gracefully...`);
207
+ stopRefresh();
208
+ const shutdownTimeout = 30000;
209
+ const startTime = Date.now();
210
+ while (processingMessages.size > 0) {
211
+ if (Date.now() - startTime > shutdownTimeout) {
212
+ logger.warn('Shutdown timeout reached, forcing exit');
213
+ break;
214
+ }
215
+ logger.info(`Waiting for ${processingMessages.size} message(s) to complete...`);
216
+ await sleep(1000);
217
+ }
218
+ logger.info('Shutdown complete');
219
+ process.exit(0);
220
+ }
221
+ process.on('SIGINT', () => shutdown('SIGINT'));
222
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
223
+ async function main() {
224
+ // Token-based auth: fetch credentials before starting
225
+ if (config.TEAMVIBE_API_URL && config.TEAMVIBE_POLLER_TOKEN) {
226
+ await initAuth(config.TEAMVIBE_API_URL, config.TEAMVIBE_POLLER_TOKEN);
227
+ }
228
+ else {
229
+ logger.info(` Queue: ${config.SQS_QUEUE_URL}`);
230
+ logger.info(` Sessions table: ${config.SESSIONS_TABLE}`);
231
+ }
232
+ await ensureDirectories();
233
+ await pollLoop();
234
+ }
235
+ main().catch((error) => {
236
+ logger.error('Fatal error:', error);
237
+ process.exit(1);
238
+ });
@@ -0,0 +1,15 @@
1
+ interface KBConfig {
2
+ kbId: string;
3
+ gitRepoUrl: string;
4
+ branch: string;
5
+ claudePath: string;
6
+ }
7
+ /**
8
+ * Get or clone KB for a persona. Returns the working directory path.
9
+ */
10
+ export declare function getKnowledgeBasePath(kb?: KBConfig): Promise<string>;
11
+ /**
12
+ * Ensure base paths exist
13
+ */
14
+ export declare function ensureDirectories(): Promise<void>;
15
+ export {};
@@ -0,0 +1,61 @@
1
+ import { exec } from 'child_process';
2
+ import { promisify } from 'util';
3
+ import { existsSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { config } from './config.js';
6
+ import { logger } from './logger.js';
7
+ const execAsync = promisify(exec);
8
+ // Cooldown tracker per KB
9
+ const lastUpdateTimes = new Map();
10
+ /**
11
+ * Get or clone KB for a persona. Returns the working directory path.
12
+ */
13
+ export async function getKnowledgeBasePath(kb) {
14
+ if (!kb?.gitRepoUrl)
15
+ return config.DEFAULT_KB_PATH;
16
+ const kbDir = join(config.KB_BASE_PATH, kb.kbId);
17
+ if (!existsSync(kbDir)) {
18
+ logger.info(`Cloning KB ${kb.kbId} from ${kb.gitRepoUrl} (branch: ${kb.branch})...`);
19
+ await execAsync(`git clone --branch ${kb.branch} ${kb.gitRepoUrl} ${kbDir}`);
20
+ logger.info(`KB ${kb.kbId} cloned successfully`);
21
+ }
22
+ else if (config.KB_AUTO_UPDATE) {
23
+ await updateKB(kbDir, kb.kbId, kb.branch);
24
+ }
25
+ return kbDir;
26
+ }
27
+ /**
28
+ * Update a KB repo (respects cooldown)
29
+ */
30
+ async function updateKB(kbDir, kbId, branch) {
31
+ const now = Date.now();
32
+ const lastUpdate = lastUpdateTimes.get(kbId) || 0;
33
+ if (now - lastUpdate < config.KB_UPDATE_COOLDOWN_MS) {
34
+ logger.debug(`KB ${kbId} update skipped - cooldown active (${Math.round((config.KB_UPDATE_COOLDOWN_MS - (now - lastUpdate)) / 1000)}s remaining)`);
35
+ return;
36
+ }
37
+ try {
38
+ logger.info(`Updating KB ${kbId} at ${kbDir}...`);
39
+ await execAsync(`git fetch origin ${branch} && git reset --hard origin/${branch}`, {
40
+ cwd: kbDir,
41
+ });
42
+ lastUpdateTimes.set(kbId, Date.now());
43
+ logger.info(`KB ${kbId} updated successfully`);
44
+ }
45
+ catch (error) {
46
+ const errorMessage = error instanceof Error ? error.message : String(error);
47
+ logger.error(`Failed to update KB ${kbId}: ${errorMessage}`);
48
+ }
49
+ }
50
+ /**
51
+ * Ensure base paths exist
52
+ */
53
+ export async function ensureDirectories() {
54
+ const { mkdirSync } = await import('fs');
55
+ if (!existsSync(config.KB_BASE_PATH)) {
56
+ mkdirSync(config.KB_BASE_PATH, { recursive: true });
57
+ }
58
+ if (!existsSync(config.DEFAULT_KB_PATH)) {
59
+ mkdirSync(config.DEFAULT_KB_PATH, { recursive: true });
60
+ }
61
+ }
@@ -0,0 +1,18 @@
1
+ export declare class SessionLogger {
2
+ private sessionId;
3
+ private logFile;
4
+ constructor(sessionId: string);
5
+ private write;
6
+ info(message: string): void;
7
+ error(message: string): void;
8
+ claude(stream: 'stdout' | 'stderr', message: string): void;
9
+ logPrompt(prompt: string): void;
10
+ getLogFile(): string;
11
+ }
12
+ export declare const logger: {
13
+ info(message: string, ...args: unknown[]): void;
14
+ error(message: string, ...args: unknown[]): void;
15
+ warn(message: string, ...args: unknown[]): void;
16
+ debug(message: string, ...args: unknown[]): void;
17
+ createSession(sessionId: string): SessionLogger;
18
+ };
package/dist/logger.js ADDED
@@ -0,0 +1,97 @@
1
+ import { appendFileSync, mkdirSync, existsSync } from 'fs';
2
+ import { dirname, join } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ const __dirname = dirname(fileURLToPath(import.meta.url));
5
+ const LOG_DIR = join(__dirname, '../.local/logs');
6
+ const SESSIONS_DIR = join(LOG_DIR, 'sessions');
7
+ const LOG_FILE = join(LOG_DIR, 'poller.log');
8
+ if (!existsSync(LOG_DIR)) {
9
+ mkdirSync(LOG_DIR, { recursive: true });
10
+ }
11
+ if (!existsSync(SESSIONS_DIR)) {
12
+ mkdirSync(SESSIONS_DIR, { recursive: true });
13
+ }
14
+ function formatDate(date) {
15
+ return date.toISOString();
16
+ }
17
+ function writeToFile(level, message, ...args) {
18
+ const timestamp = formatDate(new Date());
19
+ const formattedArgs = args
20
+ .map((arg) => (typeof arg === 'object' ? JSON.stringify(arg) : String(arg)))
21
+ .join(' ');
22
+ const logLine = `[${timestamp}] [${level}] ${message} ${formattedArgs}\n`;
23
+ try {
24
+ appendFileSync(LOG_FILE, logLine);
25
+ }
26
+ catch {
27
+ // Fallback to console if file write fails
28
+ }
29
+ }
30
+ export class SessionLogger {
31
+ sessionId;
32
+ logFile;
33
+ constructor(sessionId) {
34
+ this.sessionId = sessionId;
35
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
36
+ this.logFile = join(SESSIONS_DIR, `${timestamp}_${sessionId}.log`);
37
+ }
38
+ write(level, message) {
39
+ const timestamp = formatDate(new Date());
40
+ const logLine = `[${timestamp}] [${level}] ${message}\n`;
41
+ try {
42
+ appendFileSync(this.logFile, logLine);
43
+ }
44
+ catch {
45
+ // Silent fallback
46
+ }
47
+ }
48
+ info(message) {
49
+ console.log(`[${this.sessionId}] ${message}`);
50
+ this.write('INFO', message);
51
+ writeToFile('INFO', `[${this.sessionId}] ${message}`);
52
+ }
53
+ error(message) {
54
+ console.error(`[${this.sessionId}] ${message}`);
55
+ this.write('ERROR', message);
56
+ writeToFile('ERROR', `[${this.sessionId}] ${message}`);
57
+ }
58
+ claude(stream, message) {
59
+ const prefix = `[Claude ${stream}]`;
60
+ if (stream === 'stderr') {
61
+ console.error(`[${this.sessionId}] ${prefix}`, message);
62
+ }
63
+ else {
64
+ console.log(`[${this.sessionId}] ${prefix}`, message);
65
+ }
66
+ this.write(`CLAUDE_${stream.toUpperCase()}`, message);
67
+ }
68
+ logPrompt(prompt) {
69
+ this.write('PROMPT', `\n${'='.repeat(50)}\n${prompt}\n${'='.repeat(50)}`);
70
+ }
71
+ getLogFile() {
72
+ return this.logFile;
73
+ }
74
+ }
75
+ export const logger = {
76
+ info(message, ...args) {
77
+ console.log(message, ...args);
78
+ writeToFile('INFO', message, ...args);
79
+ },
80
+ error(message, ...args) {
81
+ console.error(message, ...args);
82
+ writeToFile('ERROR', message, ...args);
83
+ },
84
+ warn(message, ...args) {
85
+ console.warn(message, ...args);
86
+ writeToFile('WARN', message, ...args);
87
+ },
88
+ debug(message, ...args) {
89
+ if (process.env['DEBUG']) {
90
+ console.debug(message, ...args);
91
+ }
92
+ writeToFile('DEBUG', message, ...args);
93
+ },
94
+ createSession(sessionId) {
95
+ return new SessionLogger(sessionId);
96
+ },
97
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,132 @@
1
+ import { WebClient } from '@slack/web-api';
2
+ const BOT_TOKEN = process.env['SLACK_BOT_TOKEN'];
3
+ const CHANNEL = process.env['SLACK_CHANNEL'];
4
+ const THREAD_TS = process.env['SLACK_THREAD_TS'];
5
+ const MESSAGE_TS = process.env['SLACK_MESSAGE_TS'];
6
+ function fail(error) {
7
+ console.log(JSON.stringify({ ok: false, error }));
8
+ process.exit(1);
9
+ }
10
+ function succeed(data = {}) {
11
+ console.log(JSON.stringify({ ok: true, ...data }));
12
+ process.exit(0);
13
+ }
14
+ if (!BOT_TOKEN)
15
+ fail('SLACK_BOT_TOKEN not set');
16
+ const slack = new WebClient(BOT_TOKEN);
17
+ const [, , command, ...args] = process.argv;
18
+ async function main() {
19
+ switch (command) {
20
+ case 'send_message': {
21
+ const text = args.join(' ');
22
+ if (!text)
23
+ fail('Message text required');
24
+ if (!CHANNEL)
25
+ fail('SLACK_CHANNEL not set');
26
+ if (!THREAD_TS)
27
+ fail('SLACK_THREAD_TS not set');
28
+ console.error(`[slack-tool] send_message: channel=${CHANNEL}, thread_ts=${THREAD_TS}`);
29
+ const result = await slack.chat.postMessage({
30
+ channel: CHANNEL,
31
+ thread_ts: THREAD_TS,
32
+ text,
33
+ unfurl_links: false,
34
+ unfurl_media: false,
35
+ });
36
+ console.error(`[slack-tool] posted: ts=${result.ts}, thread_ts=${result.message?.thread_ts}`);
37
+ succeed({ ts: result.ts });
38
+ break;
39
+ }
40
+ case 'add_reaction': {
41
+ const emoji = args[0];
42
+ if (!emoji)
43
+ fail('Emoji name required');
44
+ if (!CHANNEL)
45
+ fail('SLACK_CHANNEL not set');
46
+ if (!MESSAGE_TS)
47
+ fail('SLACK_MESSAGE_TS not set');
48
+ await slack.reactions.add({
49
+ channel: CHANNEL,
50
+ timestamp: MESSAGE_TS,
51
+ name: emoji,
52
+ });
53
+ succeed();
54
+ break;
55
+ }
56
+ case 'remove_reaction': {
57
+ const emoji = args[0];
58
+ if (!emoji)
59
+ fail('Emoji name required');
60
+ if (!CHANNEL)
61
+ fail('SLACK_CHANNEL not set');
62
+ if (!MESSAGE_TS)
63
+ fail('SLACK_MESSAGE_TS not set');
64
+ await slack.reactions.remove({
65
+ channel: CHANNEL,
66
+ timestamp: MESSAGE_TS,
67
+ name: emoji,
68
+ });
69
+ succeed();
70
+ break;
71
+ }
72
+ case 'read_thread': {
73
+ if (!CHANNEL)
74
+ fail('SLACK_CHANNEL not set');
75
+ if (!THREAD_TS)
76
+ fail('SLACK_THREAD_TS not set');
77
+ const limit = parseInt(args[0] || '20', 10);
78
+ const result = await slack.conversations.replies({
79
+ channel: CHANNEL,
80
+ ts: THREAD_TS,
81
+ limit,
82
+ });
83
+ const messages = (result.messages || []).map((m) => ({
84
+ user: m.user || m.bot_id || 'unknown',
85
+ text: m.text || '',
86
+ ts: m.ts,
87
+ is_bot: Boolean(m.bot_id),
88
+ }));
89
+ succeed({ messages });
90
+ break;
91
+ }
92
+ case 'set_status': {
93
+ const status = args.join(' ');
94
+ if (!CHANNEL)
95
+ fail('SLACK_CHANNEL not set');
96
+ if (!THREAD_TS)
97
+ fail('SLACK_THREAD_TS not set');
98
+ await slack.apiCall('assistant.threads.setStatus', {
99
+ channel_id: CHANNEL,
100
+ thread_ts: THREAD_TS,
101
+ status,
102
+ });
103
+ succeed();
104
+ break;
105
+ }
106
+ case 'upload_snippet': {
107
+ const title = args[0];
108
+ const content = args[1];
109
+ const filetype = args[2] || 'text';
110
+ if (!title || !content)
111
+ fail('Title and content required');
112
+ if (!CHANNEL)
113
+ fail('SLACK_CHANNEL not set');
114
+ if (!THREAD_TS)
115
+ fail('SLACK_THREAD_TS not set');
116
+ await slack.filesUploadV2({
117
+ channel_id: CHANNEL,
118
+ thread_ts: THREAD_TS,
119
+ title,
120
+ content,
121
+ filetype,
122
+ });
123
+ succeed();
124
+ break;
125
+ }
126
+ default:
127
+ fail(`Unknown command: ${command}. Available: send_message, add_reaction, remove_reaction, read_thread, set_status, upload_snippet`);
128
+ }
129
+ }
130
+ main().catch((err) => {
131
+ fail(err instanceof Error ? err.message : String(err));
132
+ });
@@ -0,0 +1,11 @@
1
+ import type { SessionRecord, SessionStatus } from './types.js';
2
+ export declare function getSession(threadId: string): Promise<SessionRecord | null>;
3
+ export declare function createSession(threadId: string, workspacePath: string): Promise<SessionRecord>;
4
+ export interface AcquireLockResult {
5
+ success: boolean;
6
+ session: SessionRecord;
7
+ lockToken: string | null;
8
+ }
9
+ export declare function acquireSessionLock(threadId: string, workspacePath: string): Promise<AcquireLockResult>;
10
+ export declare function releaseSessionLock(threadId: string, lockToken: string, newStatus?: SessionStatus, lastMessageTs?: string): Promise<void>;
11
+ export declare function forceReleaseStalelock(threadId: string, maxAgeMs: number): Promise<boolean>;