@teamvibe/poller 0.1.65 → 0.1.67
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/claude-spawner.js +10 -4
- package/dist/inbox-manager.d.ts +4 -0
- package/dist/inbox-manager.js +22 -0
- package/dist/poller.js +37 -5
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/kb-manager.d.ts +0 -15
- package/dist/kb-manager.js +0 -61
- package/dist/mailbox-manager.d.ts +0 -26
- package/dist/mailbox-manager.js +0 -95
- package/dist/scripts/add-reaction.d.ts +0 -9
- package/dist/scripts/add-reaction.js +0 -22
- package/dist/scripts/lib/slack-client.d.ts +0 -16
- package/dist/scripts/lib/slack-client.js +0 -48
- package/dist/scripts/read-thread.d.ts +0 -9
- package/dist/scripts/read-thread.js +0 -26
- package/dist/scripts/remove-reaction.d.ts +0 -9
- package/dist/scripts/remove-reaction.js +0 -22
- package/dist/scripts/send-message.d.ts +0 -10
- package/dist/scripts/send-message.js +0 -31
- package/dist/scripts/set-status.d.ts +0 -8
- package/dist/scripts/set-status.js +0 -19
- package/dist/scripts/slack-tool.d.ts +0 -1
- package/dist/scripts/slack-tool.js +0 -155
- package/dist/scripts/upload-snippet.d.ts +0 -8
- package/dist/scripts/upload-snippet.js +0 -21
package/dist/claude-spawner.js
CHANGED
|
@@ -109,7 +109,8 @@ export function buildPrompt(msg, resumeHint) {
|
|
|
109
109
|
return 'heartbeat';
|
|
110
110
|
}
|
|
111
111
|
const isEmail = msg.source === 'email';
|
|
112
|
-
|
|
112
|
+
const isWebhook = msg.source === 'webhook';
|
|
113
|
+
let prompt = isEmail ? '## Incoming Email\n\n' : isWebhook ? '## Incoming Webhook\n\n' : '## Incoming Slack Message\n\n';
|
|
113
114
|
prompt += `**From:** ${msg.sender.name}`;
|
|
114
115
|
if (msg.sender.id && msg.sender.id !== msg.sender.name) {
|
|
115
116
|
prompt += ` (${msg.sender.id})`;
|
|
@@ -151,8 +152,8 @@ export function buildPrompt(msg, resumeHint) {
|
|
|
151
152
|
if (msg.type === 'view_closed' && msg.modal) {
|
|
152
153
|
prompt += `\n\n**Modal Dismissed:** User closed "${msg.modal.callbackId}" without submitting.`;
|
|
153
154
|
}
|
|
154
|
-
if (msg.teamvibe.
|
|
155
|
-
prompt += '\n\n**Channel Instructions:**\n' + msg.teamvibe.
|
|
155
|
+
if (msg.teamvibe.emailHookPrompt) {
|
|
156
|
+
prompt += '\n\n**Channel Instructions:**\n' + msg.teamvibe.emailHookPrompt + '\n';
|
|
156
157
|
}
|
|
157
158
|
if (resumeHint) {
|
|
158
159
|
prompt += `\n\n${resumeHint}`;
|
|
@@ -226,7 +227,8 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
226
227
|
const isCronMessage = msg.source === 'cron';
|
|
227
228
|
const isHeartbeat = msg.source === 'heartbeat';
|
|
228
229
|
const isEmailMessage = msg.source === 'email';
|
|
229
|
-
|
|
230
|
+
const isWebhookMessage = msg.source === 'webhook';
|
|
231
|
+
if (!slackContext && !isCronMessage && !isHeartbeat && !isEmailMessage && !isWebhookMessage) {
|
|
230
232
|
throw new Error('No Slack context in message');
|
|
231
233
|
}
|
|
232
234
|
// Build resume hint
|
|
@@ -273,7 +275,11 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
273
275
|
}
|
|
274
276
|
// Inbox protocol: instruct Claude to check for mid-task messages (thread-scoped)
|
|
275
277
|
const inboxNewDir = `.inbox/${msg.thread_id}/new`;
|
|
278
|
+
const now = new Date();
|
|
279
|
+
const todayLine = `Today: ${now.toISOString().slice(0, 10)} (${now.toLocaleDateString('en-US', { weekday: 'long' })})`;
|
|
276
280
|
args.push('--append-system-prompt', [
|
|
281
|
+
todayLine,
|
|
282
|
+
``,
|
|
277
283
|
`CRITICAL — INBOX PROTOCOL: You MUST check for new user messages after EVERY tool call.`,
|
|
278
284
|
`Run: ls ${inboxNewDir}/ 2>/dev/null`,
|
|
279
285
|
`If files exist: read each file (oldest first), delete it, and act on the message IMMEDIATELY — it takes priority over your current task.`,
|
package/dist/inbox-manager.d.ts
CHANGED
|
@@ -12,6 +12,10 @@ export declare function pickNextMessage(brainPath: string, threadId: string): {
|
|
|
12
12
|
filename: string;
|
|
13
13
|
content: string;
|
|
14
14
|
} | null;
|
|
15
|
+
/**
|
|
16
|
+
* Move a message from processing/ to failed/ after exhausting retries.
|
|
17
|
+
*/
|
|
18
|
+
export declare function markFailed(brainPath: string, threadId: string, filename: string): void;
|
|
15
19
|
/**
|
|
16
20
|
* Delete a message file from processing/ (marks it as done).
|
|
17
21
|
*/
|
package/dist/inbox-manager.js
CHANGED
|
@@ -4,6 +4,7 @@ import { logger } from './logger.js';
|
|
|
4
4
|
const INBOX_DIR = '.inbox';
|
|
5
5
|
const NEW_DIR = 'new';
|
|
6
6
|
const PROCESSING_DIR = 'processing';
|
|
7
|
+
const FAILED_DIR = 'failed';
|
|
7
8
|
function getInboxPath(brainPath, threadId) {
|
|
8
9
|
return join(brainPath, INBOX_DIR, threadId);
|
|
9
10
|
}
|
|
@@ -56,6 +57,27 @@ export function pickNextMessage(brainPath, threadId) {
|
|
|
56
57
|
}
|
|
57
58
|
return null;
|
|
58
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* Move a message from processing/ to failed/ after exhausting retries.
|
|
62
|
+
*/
|
|
63
|
+
export function markFailed(brainPath, threadId, filename) {
|
|
64
|
+
const inbox = getInboxPath(brainPath, threadId);
|
|
65
|
+
const srcPath = join(inbox, PROCESSING_DIR, filename);
|
|
66
|
+
const failedDir = join(inbox, FAILED_DIR);
|
|
67
|
+
mkdirSync(failedDir, { recursive: true });
|
|
68
|
+
const dstPath = join(failedDir, filename);
|
|
69
|
+
try {
|
|
70
|
+
if (existsSync(srcPath)) {
|
|
71
|
+
renameSync(srcPath, dstPath);
|
|
72
|
+
logger.info(`[Inbox] Moved exhausted message to failed/: ${filename}`);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
logger.warn(`[Inbox] Failed to move ${filename} to failed/: ${error}`);
|
|
77
|
+
// Fallback: delete so the loop doesn't get stuck
|
|
78
|
+
markProcessed(brainPath, threadId, filename);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
59
81
|
/**
|
|
60
82
|
* Delete a message file from processing/ (marks it as done).
|
|
61
83
|
*/
|
package/dist/poller.js
CHANGED
|
@@ -9,7 +9,7 @@ import { postMaintenanceReports } from './maintenance-reporter.js';
|
|
|
9
9
|
import { initAuth, stopRefresh } from './auth-provider.js';
|
|
10
10
|
import { registerBrain, startHeartbeatLoop, stopHeartbeatLoop } from './heartbeat-manager.js';
|
|
11
11
|
import { startEventQueue, stopEventQueue, flush as flushEventQueue, emitEvent } from './event-queue.js';
|
|
12
|
-
import { writeMessage, pickNextMessage, markProcessed, hasUnprocessed } from './inbox-manager.js';
|
|
12
|
+
import { writeMessage, pickNextMessage, markProcessed, markFailed, hasUnprocessed } from './inbox-manager.js';
|
|
13
13
|
// Track active message processing
|
|
14
14
|
const processingMessages = new Set();
|
|
15
15
|
// Track which threads have an active processing loop
|
|
@@ -21,6 +21,7 @@ async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
|
|
|
21
21
|
}
|
|
22
22
|
activeLoops.add(threadId);
|
|
23
23
|
let iteration = 0;
|
|
24
|
+
const failureCounts = new Map();
|
|
24
25
|
try {
|
|
25
26
|
let lockResult = await acquireSessionLock(threadId, kbPath);
|
|
26
27
|
if (!lockResult.success) {
|
|
@@ -28,6 +29,7 @@ async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
|
|
|
28
29
|
return;
|
|
29
30
|
}
|
|
30
31
|
const { session, lockToken } = lockResult;
|
|
32
|
+
let currentSessionId = session.session_id || undefined;
|
|
31
33
|
while (true) {
|
|
32
34
|
const message = pickNextMessage(kbPath, threadId);
|
|
33
35
|
if (!message) {
|
|
@@ -44,6 +46,7 @@ async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
|
|
|
44
46
|
? startTypingIndicator(queueMessage)
|
|
45
47
|
: undefined;
|
|
46
48
|
let succeeded = false;
|
|
49
|
+
let lastExitCode = null;
|
|
47
50
|
try {
|
|
48
51
|
// Reconstruct queue message from stored inbox data
|
|
49
52
|
const storedData = JSON.parse(message.content);
|
|
@@ -51,10 +54,21 @@ async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
|
|
|
51
54
|
...queueMessage,
|
|
52
55
|
...storedData,
|
|
53
56
|
};
|
|
54
|
-
const result = await spawnClaudeCode(inboxMsg, sessionLog, kbPath,
|
|
57
|
+
const result = await spawnClaudeCode(inboxMsg, sessionLog, kbPath, currentSessionId, isFirstMessage, session.last_message_ts, () => typing?.stop(), typing);
|
|
55
58
|
typing?.stop();
|
|
56
|
-
if (result.newSessionId
|
|
57
|
-
|
|
59
|
+
if (result.newSessionId) {
|
|
60
|
+
// Update local state first — DynamoDB writes may fail (expired token,
|
|
61
|
+
// network), but subsequent iterations MUST use the fresh session ID
|
|
62
|
+
// to avoid "session already in use" retry loops.
|
|
63
|
+
currentSessionId = result.newSessionId;
|
|
64
|
+
if (lockToken) {
|
|
65
|
+
try {
|
|
66
|
+
await updateSessionId(threadId, lockToken, result.newSessionId);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
sessionLog.error(`[Loop] Failed to persist new session ID to DynamoDB: ${err instanceof Error ? err.message : err}`);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
58
72
|
}
|
|
59
73
|
if (result.metrics) {
|
|
60
74
|
sessionLog.info(`[Loop] Iteration #${iteration} complete — ` +
|
|
@@ -84,6 +98,7 @@ async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
|
|
|
84
98
|
await postMaintenanceReports(kbPath, queueMessage.teamvibe.channelId);
|
|
85
99
|
}
|
|
86
100
|
else {
|
|
101
|
+
lastExitCode = result.exitCode;
|
|
87
102
|
sessionLog.error(`[Loop] Claude failed iteration #${iteration}: ${result.error}`);
|
|
88
103
|
if (hasSlackContext) {
|
|
89
104
|
const errorMessage = result.error || `Process exited with code ${result.exitCode}`;
|
|
@@ -97,10 +112,27 @@ async function runInboxLoop(threadId, kbPath, queueMessage, sessionLog) {
|
|
|
97
112
|
typing?.stop();
|
|
98
113
|
sessionLog.error(`[Loop] Error in iteration #${iteration}: ${error instanceof Error ? error.message : error}`);
|
|
99
114
|
}
|
|
100
|
-
// Only delete processing file on success — failed/killed sessions get retried
|
|
101
115
|
if (succeeded) {
|
|
102
116
|
markProcessed(kbPath, threadId, message.filename);
|
|
103
117
|
}
|
|
118
|
+
else {
|
|
119
|
+
// SIGTERM (143) or SIGKILL (137) = intentional kill → retry immediately, no backoff
|
|
120
|
+
const isKilledSignal = lastExitCode === 143 || lastExitCode === 137;
|
|
121
|
+
if (!isKilledSignal) {
|
|
122
|
+
const failures = (failureCounts.get(message.filename) || 0) + 1;
|
|
123
|
+
failureCounts.set(message.filename, failures);
|
|
124
|
+
const MAX_RETRIES = 3;
|
|
125
|
+
if (failures >= MAX_RETRIES) {
|
|
126
|
+
sessionLog.error(`[Loop] Message ${message.filename} failed ${failures} times, giving up`);
|
|
127
|
+
markFailed(kbPath, threadId, message.filename);
|
|
128
|
+
}
|
|
129
|
+
else {
|
|
130
|
+
const backoffMs = Math.min(10_000 * Math.pow(2, failures - 1), 60_000);
|
|
131
|
+
sessionLog.info(`[Loop] Retry ${failures}/${MAX_RETRIES - 1} for ${message.filename}, backing off ${backoffMs}ms`);
|
|
132
|
+
await sleep(backoffMs);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
}
|
|
104
136
|
if (!hasUnprocessed(kbPath, threadId)) {
|
|
105
137
|
break;
|
|
106
138
|
}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export interface TeamVibeQueueMessage {
|
|
2
2
|
id: string;
|
|
3
|
-
source: 'slack' | 'email' | 'cron' | 'heartbeat';
|
|
3
|
+
source: 'slack' | 'email' | 'cron' | 'heartbeat' | 'webhook';
|
|
4
4
|
thread_id: string;
|
|
5
5
|
sender: {
|
|
6
6
|
id: string;
|
|
@@ -50,7 +50,7 @@ export interface TeamVibeQueueMessage {
|
|
|
50
50
|
gitRepoUrl: string;
|
|
51
51
|
branch: string;
|
|
52
52
|
};
|
|
53
|
-
|
|
53
|
+
emailHookPrompt?: string;
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
export interface SessionRecord {
|
package/package.json
CHANGED
package/dist/kb-manager.d.ts
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
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 {};
|
package/dist/kb-manager.js
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Write a new message to the mailbox.
|
|
3
|
-
* Returns the filename of the created message file.
|
|
4
|
-
*/
|
|
5
|
-
export declare function writeMessage(brainPath: string, threadId: string, content: string): string;
|
|
6
|
-
/**
|
|
7
|
-
* Pick the oldest message from processing/ (crash recovery) or new/.
|
|
8
|
-
* If found in new/, moves it to processing/.
|
|
9
|
-
* Returns { filename, content } or null if no messages.
|
|
10
|
-
*/
|
|
11
|
-
export declare function pickNextMessage(brainPath: string, threadId: string): {
|
|
12
|
-
filename: string;
|
|
13
|
-
content: string;
|
|
14
|
-
} | null;
|
|
15
|
-
/**
|
|
16
|
-
* Delete a message file from processing/ (marks it as done).
|
|
17
|
-
*/
|
|
18
|
-
export declare function markProcessed(brainPath: string, threadId: string, filename: string): void;
|
|
19
|
-
/**
|
|
20
|
-
* Check if there are unprocessed messages in new/ or processing/.
|
|
21
|
-
*/
|
|
22
|
-
export declare function hasUnprocessed(brainPath: string, threadId: string): boolean;
|
|
23
|
-
/**
|
|
24
|
-
* Get the mailbox new/ directory path for use in Claude's system prompt.
|
|
25
|
-
*/
|
|
26
|
-
export declare function getMailboxNewPath(brainPath: string, threadId: string): string;
|
package/dist/mailbox-manager.js
DELETED
|
@@ -1,95 +0,0 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync, readFileSync, renameSync, unlinkSync, readdirSync, existsSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
|
-
import { logger } from './logger.js';
|
|
4
|
-
const MAILBOX_DIR = '.mailbox';
|
|
5
|
-
const NEW_DIR = 'new';
|
|
6
|
-
const PROCESSING_DIR = 'processing';
|
|
7
|
-
function getMailboxPath(brainPath, threadId) {
|
|
8
|
-
return join(brainPath, MAILBOX_DIR, threadId);
|
|
9
|
-
}
|
|
10
|
-
function ensureMailboxDirs(brainPath, threadId) {
|
|
11
|
-
const mailbox = getMailboxPath(brainPath, threadId);
|
|
12
|
-
mkdirSync(join(mailbox, NEW_DIR), { recursive: true });
|
|
13
|
-
mkdirSync(join(mailbox, PROCESSING_DIR), { recursive: true });
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Write a new message to the mailbox.
|
|
17
|
-
* Returns the filename of the created message file.
|
|
18
|
-
*/
|
|
19
|
-
export function writeMessage(brainPath, threadId, content) {
|
|
20
|
-
ensureMailboxDirs(brainPath, threadId);
|
|
21
|
-
const timestamp = process.hrtime.bigint().toString();
|
|
22
|
-
const filename = `${timestamp}.txt`;
|
|
23
|
-
const filepath = join(getMailboxPath(brainPath, threadId), NEW_DIR, filename);
|
|
24
|
-
writeFileSync(filepath, content, 'utf-8');
|
|
25
|
-
logger.info(`[Mailbox] Wrote message: ${filename} to ${getMailboxPath(brainPath, threadId)}/new/ (${content.length} chars, threadId=${threadId})`);
|
|
26
|
-
return filename;
|
|
27
|
-
}
|
|
28
|
-
/**
|
|
29
|
-
* Pick the oldest message from processing/ (crash recovery) or new/.
|
|
30
|
-
* If found in new/, moves it to processing/.
|
|
31
|
-
* Returns { filename, content } or null if no messages.
|
|
32
|
-
*/
|
|
33
|
-
export function pickNextMessage(brainPath, threadId) {
|
|
34
|
-
ensureMailboxDirs(brainPath, threadId);
|
|
35
|
-
const mailbox = getMailboxPath(brainPath, threadId);
|
|
36
|
-
const processingDir = join(mailbox, PROCESSING_DIR);
|
|
37
|
-
const newDir = join(mailbox, NEW_DIR);
|
|
38
|
-
// First check processing/ (crash recovery)
|
|
39
|
-
const processingFiles = readdirSync(processingDir).filter(f => f.endsWith('.txt')).sort();
|
|
40
|
-
if (processingFiles.length > 0) {
|
|
41
|
-
const filename = processingFiles[0];
|
|
42
|
-
const content = readFileSync(join(processingDir, filename), 'utf-8');
|
|
43
|
-
logger.info(`[Mailbox] Resuming processing message: ${filename}`);
|
|
44
|
-
return { filename, content };
|
|
45
|
-
}
|
|
46
|
-
// Then check new/
|
|
47
|
-
const newFiles = readdirSync(newDir).filter(f => f.endsWith('.txt')).sort();
|
|
48
|
-
if (newFiles.length > 0) {
|
|
49
|
-
const filename = newFiles[0];
|
|
50
|
-
const srcPath = join(newDir, filename);
|
|
51
|
-
const dstPath = join(processingDir, filename);
|
|
52
|
-
const content = readFileSync(srcPath, 'utf-8');
|
|
53
|
-
renameSync(srcPath, dstPath);
|
|
54
|
-
logger.info(`[Mailbox] Picked message: ${filename} (moved to processing)`);
|
|
55
|
-
return { filename, content };
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
58
|
-
}
|
|
59
|
-
/**
|
|
60
|
-
* Delete a message file from processing/ (marks it as done).
|
|
61
|
-
*/
|
|
62
|
-
export function markProcessed(brainPath, threadId, filename) {
|
|
63
|
-
const filepath = join(getMailboxPath(brainPath, threadId), PROCESSING_DIR, filename);
|
|
64
|
-
try {
|
|
65
|
-
if (existsSync(filepath)) {
|
|
66
|
-
unlinkSync(filepath);
|
|
67
|
-
logger.info(`[Mailbox] Deleted processed message: ${filename}`);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
catch (error) {
|
|
71
|
-
logger.warn(`[Mailbox] Failed to delete ${filename}: ${error}`);
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Check if there are unprocessed messages in new/ or processing/.
|
|
76
|
-
*/
|
|
77
|
-
export function hasUnprocessed(brainPath, threadId) {
|
|
78
|
-
const mailbox = getMailboxPath(brainPath, threadId);
|
|
79
|
-
const newDir = join(mailbox, NEW_DIR);
|
|
80
|
-
const processingDir = join(mailbox, PROCESSING_DIR);
|
|
81
|
-
try {
|
|
82
|
-
const newCount = existsSync(newDir) ? readdirSync(newDir).filter(f => f.endsWith('.txt')).length : 0;
|
|
83
|
-
const procCount = existsSync(processingDir) ? readdirSync(processingDir).filter(f => f.endsWith('.txt')).length : 0;
|
|
84
|
-
return (newCount + procCount) > 0;
|
|
85
|
-
}
|
|
86
|
-
catch {
|
|
87
|
-
return false;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
/**
|
|
91
|
-
* Get the mailbox new/ directory path for use in Claude's system prompt.
|
|
92
|
-
*/
|
|
93
|
-
export function getMailboxNewPath(brainPath, threadId) {
|
|
94
|
-
return join(getMailboxPath(brainPath, threadId), NEW_DIR);
|
|
95
|
-
}
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Add an emoji reaction to the original message.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node add-reaction.js emoji_name
|
|
7
|
-
* node add-reaction.js --channel C123 --message_ts 123.456 emoji_name
|
|
8
|
-
*/
|
|
9
|
-
import { slack, fail, succeed, parseArgs } from './lib/slack-client.js';
|
|
10
|
-
const { channel, messageTs, positional } = parseArgs();
|
|
11
|
-
const emoji = positional[0];
|
|
12
|
-
if (!emoji)
|
|
13
|
-
fail('Emoji name required');
|
|
14
|
-
if (!channel)
|
|
15
|
-
fail('Channel required (--channel or SLACK_CHANNEL env)');
|
|
16
|
-
if (!messageTs)
|
|
17
|
-
fail('Message TS required (--message_ts or SLACK_MESSAGE_TS env)');
|
|
18
|
-
async function main() {
|
|
19
|
-
await slack.reactions.add({ channel: channel, timestamp: messageTs, name: emoji });
|
|
20
|
-
succeed();
|
|
21
|
-
}
|
|
22
|
-
main().catch((err) => fail(err instanceof Error ? err.message : String(err)));
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
import { WebClient } from '@slack/web-api';
|
|
2
|
-
export declare function fail(error: string): never;
|
|
3
|
-
export declare function succeed(data?: Record<string, unknown>): never;
|
|
4
|
-
export declare const slack: WebClient;
|
|
5
|
-
/**
|
|
6
|
-
* Parse CLI args: extracts known --flags and returns remaining positional args.
|
|
7
|
-
* Supports both --flag value and --flag=value syntax.
|
|
8
|
-
* Falls back to env vars for channel/thread_ts/message_ts.
|
|
9
|
-
*/
|
|
10
|
-
export declare function parseArgs(argv?: string[]): {
|
|
11
|
-
channel: string | undefined;
|
|
12
|
-
threadTs: string | undefined;
|
|
13
|
-
messageTs: string | undefined;
|
|
14
|
-
positional: string[];
|
|
15
|
-
textFromFlag: string | undefined;
|
|
16
|
-
};
|
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { WebClient } from '@slack/web-api';
|
|
2
|
-
const BOT_TOKEN = process.env['SLACK_BOT_TOKEN'];
|
|
3
|
-
export function fail(error) {
|
|
4
|
-
console.log(JSON.stringify({ ok: false, error }));
|
|
5
|
-
process.exit(1);
|
|
6
|
-
}
|
|
7
|
-
export function succeed(data = {}) {
|
|
8
|
-
console.log(JSON.stringify({ ok: true, ...data }));
|
|
9
|
-
process.exit(0);
|
|
10
|
-
}
|
|
11
|
-
if (!BOT_TOKEN)
|
|
12
|
-
fail('SLACK_BOT_TOKEN not set');
|
|
13
|
-
export const slack = new WebClient(BOT_TOKEN);
|
|
14
|
-
/**
|
|
15
|
-
* Parse CLI args: extracts known --flags and returns remaining positional args.
|
|
16
|
-
* Supports both --flag value and --flag=value syntax.
|
|
17
|
-
* Falls back to env vars for channel/thread_ts/message_ts.
|
|
18
|
-
*/
|
|
19
|
-
export function parseArgs(argv = process.argv.slice(2)) {
|
|
20
|
-
const flags = {};
|
|
21
|
-
const positional = [];
|
|
22
|
-
const knownFlags = ['--channel', '--thread_ts', '--thread-ts', '--message_ts', '--message-ts', '--text'];
|
|
23
|
-
let i = 0;
|
|
24
|
-
while (i < argv.length) {
|
|
25
|
-
const arg = argv[i];
|
|
26
|
-
// --flag=value
|
|
27
|
-
const eqMatch = arg.match(/^(--[\w-]+)=(.*)$/);
|
|
28
|
-
if (eqMatch) {
|
|
29
|
-
flags[eqMatch[1]] = eqMatch[2];
|
|
30
|
-
i++;
|
|
31
|
-
continue;
|
|
32
|
-
}
|
|
33
|
-
// --flag value
|
|
34
|
-
if (knownFlags.includes(arg) && i + 1 < argv.length) {
|
|
35
|
-
flags[arg] = argv[i + 1];
|
|
36
|
-
i += 2;
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
positional.push(arg);
|
|
40
|
-
i++;
|
|
41
|
-
}
|
|
42
|
-
// If --text was provided, use it (and any remaining positional after it)
|
|
43
|
-
const textFromFlag = flags['--text'];
|
|
44
|
-
const channel = flags['--channel'] || process.env['SLACK_CHANNEL'];
|
|
45
|
-
const threadTs = flags['--thread_ts'] || flags['--thread-ts'] || process.env['SLACK_THREAD_TS'];
|
|
46
|
-
const messageTs = flags['--message_ts'] || flags['--message-ts'] || process.env['SLACK_MESSAGE_TS'];
|
|
47
|
-
return { channel, threadTs, messageTs, positional, textFromFlag };
|
|
48
|
-
}
|
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Read thread history from a Slack thread.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node read-thread.js [limit]
|
|
7
|
-
* node read-thread.js --channel C123 --thread_ts 123.456 [limit]
|
|
8
|
-
*/
|
|
9
|
-
import { slack, fail, succeed, parseArgs } from './lib/slack-client.js';
|
|
10
|
-
const { channel, threadTs, positional } = parseArgs();
|
|
11
|
-
const limit = parseInt(positional[0] || '20', 10);
|
|
12
|
-
if (!channel)
|
|
13
|
-
fail('Channel required (--channel or SLACK_CHANNEL env)');
|
|
14
|
-
if (!threadTs)
|
|
15
|
-
fail('Thread TS required (--thread_ts or SLACK_THREAD_TS env)');
|
|
16
|
-
async function main() {
|
|
17
|
-
const result = await slack.conversations.replies({ channel: channel, ts: threadTs, limit });
|
|
18
|
-
const messages = (result.messages || []).map((m) => ({
|
|
19
|
-
user: m.user || m.bot_id || 'unknown',
|
|
20
|
-
text: m.text || '',
|
|
21
|
-
ts: m.ts,
|
|
22
|
-
is_bot: Boolean(m.bot_id),
|
|
23
|
-
}));
|
|
24
|
-
succeed({ messages });
|
|
25
|
-
}
|
|
26
|
-
main().catch((err) => fail(err instanceof Error ? err.message : String(err)));
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Remove an emoji reaction from the original message.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node remove-reaction.js emoji_name
|
|
7
|
-
* node remove-reaction.js --channel C123 --message_ts 123.456 emoji_name
|
|
8
|
-
*/
|
|
9
|
-
import { slack, fail, succeed, parseArgs } from './lib/slack-client.js';
|
|
10
|
-
const { channel, messageTs, positional } = parseArgs();
|
|
11
|
-
const emoji = positional[0];
|
|
12
|
-
if (!emoji)
|
|
13
|
-
fail('Emoji name required');
|
|
14
|
-
if (!channel)
|
|
15
|
-
fail('Channel required (--channel or SLACK_CHANNEL env)');
|
|
16
|
-
if (!messageTs)
|
|
17
|
-
fail('Message TS required (--message_ts or SLACK_MESSAGE_TS env)');
|
|
18
|
-
async function main() {
|
|
19
|
-
await slack.reactions.remove({ channel: channel, timestamp: messageTs, name: emoji });
|
|
20
|
-
succeed();
|
|
21
|
-
}
|
|
22
|
-
main().catch((err) => fail(err instanceof Error ? err.message : String(err)));
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Send a message to a Slack thread.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node send-message.js "Your message here"
|
|
7
|
-
* node send-message.js --text "Your message here"
|
|
8
|
-
* node send-message.js --channel C123 --thread_ts 123.456 "Your message"
|
|
9
|
-
*/
|
|
10
|
-
export {};
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Send a message to a Slack thread.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node send-message.js "Your message here"
|
|
7
|
-
* node send-message.js --text "Your message here"
|
|
8
|
-
* node send-message.js --channel C123 --thread_ts 123.456 "Your message"
|
|
9
|
-
*/
|
|
10
|
-
import { slack, fail, succeed, parseArgs } from './lib/slack-client.js';
|
|
11
|
-
const { channel, threadTs, positional, textFromFlag } = parseArgs();
|
|
12
|
-
const text = textFromFlag || positional.join(' ');
|
|
13
|
-
if (!text)
|
|
14
|
-
fail('Message text required');
|
|
15
|
-
if (!channel)
|
|
16
|
-
fail('Channel required (--channel or SLACK_CHANNEL env)');
|
|
17
|
-
if (!threadTs)
|
|
18
|
-
fail('Thread TS required (--thread_ts or SLACK_THREAD_TS env)');
|
|
19
|
-
async function main() {
|
|
20
|
-
console.error(`[slack] send_message: channel=${channel}, thread_ts=${threadTs}`);
|
|
21
|
-
const result = await slack.chat.postMessage({
|
|
22
|
-
channel: channel,
|
|
23
|
-
thread_ts: threadTs,
|
|
24
|
-
text,
|
|
25
|
-
unfurl_links: false,
|
|
26
|
-
unfurl_media: false,
|
|
27
|
-
});
|
|
28
|
-
console.error(`[slack] posted: ts=${result.ts}`);
|
|
29
|
-
succeed({ ts: result.ts });
|
|
30
|
-
}
|
|
31
|
-
main().catch((err) => fail(err instanceof Error ? err.message : String(err)));
|
|
@@ -1,19 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Set the assistant thread status.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node set-status.js "Status text"
|
|
7
|
-
*/
|
|
8
|
-
import { slack, fail, succeed, parseArgs } from './lib/slack-client.js';
|
|
9
|
-
const { channel, threadTs, positional } = parseArgs();
|
|
10
|
-
const status = positional.join(' ');
|
|
11
|
-
if (!channel)
|
|
12
|
-
fail('Channel required');
|
|
13
|
-
if (!threadTs)
|
|
14
|
-
fail('Thread TS required');
|
|
15
|
-
async function main() {
|
|
16
|
-
await slack.apiCall('assistant.threads.setStatus', { channel_id: channel, thread_ts: threadTs, status });
|
|
17
|
-
succeed();
|
|
18
|
-
}
|
|
19
|
-
main().catch((err) => fail(err instanceof Error ? err.message : String(err)));
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export {};
|
|
@@ -1,155 +0,0 @@
|
|
|
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
|
-
/**
|
|
19
|
-
* Strip --channel, --thread_ts, --text flags that Claude sometimes adds.
|
|
20
|
-
* These values come from env vars, not CLI args.
|
|
21
|
-
*/
|
|
22
|
-
function stripFlags(rawArgs) {
|
|
23
|
-
const flagsToStrip = ['--channel', '--thread_ts', '--thread-ts', '--text'];
|
|
24
|
-
const cleaned = [];
|
|
25
|
-
let i = 0;
|
|
26
|
-
while (i < rawArgs.length) {
|
|
27
|
-
if (flagsToStrip.includes(rawArgs[i])) {
|
|
28
|
-
// If this is --text, everything after the next arg is the message
|
|
29
|
-
if (rawArgs[i] === '--text') {
|
|
30
|
-
return rawArgs.slice(i + 1).join(' ');
|
|
31
|
-
}
|
|
32
|
-
i += 2; // skip flag + value
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
cleaned.push(rawArgs[i]);
|
|
36
|
-
i++;
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
return cleaned.join(' ');
|
|
40
|
-
}
|
|
41
|
-
async function main() {
|
|
42
|
-
switch (command) {
|
|
43
|
-
case 'send_message': {
|
|
44
|
-
const text = stripFlags(args);
|
|
45
|
-
if (!text)
|
|
46
|
-
fail('Message text required');
|
|
47
|
-
if (!CHANNEL)
|
|
48
|
-
fail('SLACK_CHANNEL not set');
|
|
49
|
-
if (!THREAD_TS)
|
|
50
|
-
fail('SLACK_THREAD_TS not set');
|
|
51
|
-
console.error(`[slack-tool] send_message: channel=${CHANNEL}, thread_ts=${THREAD_TS}`);
|
|
52
|
-
const result = await slack.chat.postMessage({
|
|
53
|
-
channel: CHANNEL,
|
|
54
|
-
thread_ts: THREAD_TS,
|
|
55
|
-
text,
|
|
56
|
-
unfurl_links: false,
|
|
57
|
-
unfurl_media: false,
|
|
58
|
-
});
|
|
59
|
-
console.error(`[slack-tool] posted: ts=${result.ts}, thread_ts=${result.message?.thread_ts}`);
|
|
60
|
-
succeed({ ts: result.ts });
|
|
61
|
-
break;
|
|
62
|
-
}
|
|
63
|
-
case 'add_reaction': {
|
|
64
|
-
const emoji = args[0];
|
|
65
|
-
if (!emoji)
|
|
66
|
-
fail('Emoji name required');
|
|
67
|
-
if (!CHANNEL)
|
|
68
|
-
fail('SLACK_CHANNEL not set');
|
|
69
|
-
if (!MESSAGE_TS)
|
|
70
|
-
fail('SLACK_MESSAGE_TS not set');
|
|
71
|
-
await slack.reactions.add({
|
|
72
|
-
channel: CHANNEL,
|
|
73
|
-
timestamp: MESSAGE_TS,
|
|
74
|
-
name: emoji,
|
|
75
|
-
});
|
|
76
|
-
succeed();
|
|
77
|
-
break;
|
|
78
|
-
}
|
|
79
|
-
case 'remove_reaction': {
|
|
80
|
-
const emoji = args[0];
|
|
81
|
-
if (!emoji)
|
|
82
|
-
fail('Emoji name required');
|
|
83
|
-
if (!CHANNEL)
|
|
84
|
-
fail('SLACK_CHANNEL not set');
|
|
85
|
-
if (!MESSAGE_TS)
|
|
86
|
-
fail('SLACK_MESSAGE_TS not set');
|
|
87
|
-
await slack.reactions.remove({
|
|
88
|
-
channel: CHANNEL,
|
|
89
|
-
timestamp: MESSAGE_TS,
|
|
90
|
-
name: emoji,
|
|
91
|
-
});
|
|
92
|
-
succeed();
|
|
93
|
-
break;
|
|
94
|
-
}
|
|
95
|
-
case 'read_thread': {
|
|
96
|
-
if (!CHANNEL)
|
|
97
|
-
fail('SLACK_CHANNEL not set');
|
|
98
|
-
if (!THREAD_TS)
|
|
99
|
-
fail('SLACK_THREAD_TS not set');
|
|
100
|
-
const limit = parseInt(args[0] || '20', 10);
|
|
101
|
-
const result = await slack.conversations.replies({
|
|
102
|
-
channel: CHANNEL,
|
|
103
|
-
ts: THREAD_TS,
|
|
104
|
-
limit,
|
|
105
|
-
});
|
|
106
|
-
const messages = (result.messages || []).map((m) => ({
|
|
107
|
-
user: m.user || m.bot_id || 'unknown',
|
|
108
|
-
text: m.text || '',
|
|
109
|
-
ts: m.ts,
|
|
110
|
-
is_bot: Boolean(m.bot_id),
|
|
111
|
-
}));
|
|
112
|
-
succeed({ messages });
|
|
113
|
-
break;
|
|
114
|
-
}
|
|
115
|
-
case 'set_status': {
|
|
116
|
-
const status = args.join(' ');
|
|
117
|
-
if (!CHANNEL)
|
|
118
|
-
fail('SLACK_CHANNEL not set');
|
|
119
|
-
if (!THREAD_TS)
|
|
120
|
-
fail('SLACK_THREAD_TS not set');
|
|
121
|
-
await slack.apiCall('assistant.threads.setStatus', {
|
|
122
|
-
channel_id: CHANNEL,
|
|
123
|
-
thread_ts: THREAD_TS,
|
|
124
|
-
status,
|
|
125
|
-
});
|
|
126
|
-
succeed();
|
|
127
|
-
break;
|
|
128
|
-
}
|
|
129
|
-
case 'upload_snippet': {
|
|
130
|
-
const title = args[0];
|
|
131
|
-
const content = args[1];
|
|
132
|
-
const filetype = args[2] || 'text';
|
|
133
|
-
if (!title || !content)
|
|
134
|
-
fail('Title and content required');
|
|
135
|
-
if (!CHANNEL)
|
|
136
|
-
fail('SLACK_CHANNEL not set');
|
|
137
|
-
if (!THREAD_TS)
|
|
138
|
-
fail('SLACK_THREAD_TS not set');
|
|
139
|
-
await slack.filesUploadV2({
|
|
140
|
-
channel_id: CHANNEL,
|
|
141
|
-
thread_ts: THREAD_TS,
|
|
142
|
-
title,
|
|
143
|
-
content,
|
|
144
|
-
filetype,
|
|
145
|
-
});
|
|
146
|
-
succeed();
|
|
147
|
-
break;
|
|
148
|
-
}
|
|
149
|
-
default:
|
|
150
|
-
fail(`Unknown command: ${command}. Available: send_message, add_reaction, remove_reaction, read_thread, set_status, upload_snippet`);
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
main().catch((err) => {
|
|
154
|
-
fail(err instanceof Error ? err.message : String(err));
|
|
155
|
-
});
|
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Upload a code/text snippet to a Slack thread.
|
|
4
|
-
*
|
|
5
|
-
* Usage:
|
|
6
|
-
* node upload-snippet.js "title" "content" [filetype]
|
|
7
|
-
*/
|
|
8
|
-
import { slack, fail, succeed, parseArgs } from './lib/slack-client.js';
|
|
9
|
-
const { channel, threadTs, positional } = parseArgs();
|
|
10
|
-
const [title, content, filetype = 'text'] = positional;
|
|
11
|
-
if (!title || !content)
|
|
12
|
-
fail('Title and content required');
|
|
13
|
-
if (!channel)
|
|
14
|
-
fail('Channel required');
|
|
15
|
-
if (!threadTs)
|
|
16
|
-
fail('Thread TS required');
|
|
17
|
-
async function main() {
|
|
18
|
-
await slack.filesUploadV2({ channel_id: channel, thread_ts: threadTs, title: title, content: content, filetype });
|
|
19
|
-
succeed();
|
|
20
|
-
}
|
|
21
|
-
main().catch((err) => fail(err instanceof Error ? err.message : String(err)));
|