@teamvibe/poller 0.1.6 → 0.1.7
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/brain-manager.d.ts +29 -0
- package/dist/brain-manager.js +130 -0
- package/dist/claude-spawner.d.ts +1 -1
- package/dist/claude-spawner.js +36 -70
- package/dist/config.d.ts +32 -16
- package/dist/config.js +13 -7
- package/dist/index.js +11 -6
- package/dist/scripts/add-reaction.d.ts +9 -0
- package/dist/scripts/add-reaction.js +22 -0
- package/dist/scripts/lib/slack-client.d.ts +16 -0
- package/dist/scripts/lib/slack-client.js +48 -0
- package/dist/scripts/read-thread.d.ts +9 -0
- package/dist/scripts/read-thread.js +26 -0
- package/dist/scripts/remove-reaction.d.ts +9 -0
- package/dist/scripts/remove-reaction.js +22 -0
- package/dist/scripts/send-message.d.ts +10 -0
- package/dist/scripts/send-message.js +31 -0
- package/dist/scripts/set-status.d.ts +8 -0
- package/dist/scripts/set-status.js +19 -0
- package/dist/scripts/slack-tool.js +24 -1
- package/dist/scripts/upload-snippet.d.ts +8 -0
- package/dist/scripts/upload-snippet.js +21 -0
- package/dist/types.d.ts +5 -10
- package/package.json +1 -1
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
interface BrainConfig {
|
|
2
|
+
brainId: string;
|
|
3
|
+
gitRepoUrl: string;
|
|
4
|
+
branch: string;
|
|
5
|
+
claudePath: string;
|
|
6
|
+
}
|
|
7
|
+
/**
|
|
8
|
+
* Get or clone brain repo. Returns the working directory path.
|
|
9
|
+
*/
|
|
10
|
+
export declare function getBrainPath(brain?: BrainConfig): Promise<string>;
|
|
11
|
+
/**
|
|
12
|
+
* Ensure base brain repo is cloned and up to date.
|
|
13
|
+
* Uses same cooldown pattern as channel brains.
|
|
14
|
+
*/
|
|
15
|
+
export declare function ensureBaseBrain(): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Get the path to the base brain directory.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getBaseBrainPath(): string;
|
|
20
|
+
/**
|
|
21
|
+
* Commit and push any changes in a brain repo.
|
|
22
|
+
* Silently skips if there are no changes or no remote.
|
|
23
|
+
*/
|
|
24
|
+
export declare function pushBrainChanges(brainDir: string, brainId: string): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Ensure base paths exist
|
|
27
|
+
*/
|
|
28
|
+
export declare function ensureDirectories(): Promise<void>;
|
|
29
|
+
export {};
|
|
@@ -0,0 +1,130 @@
|
|
|
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 brain
|
|
9
|
+
const lastUpdateTimes = new Map();
|
|
10
|
+
/**
|
|
11
|
+
* Get or clone brain repo. Returns the working directory path.
|
|
12
|
+
*/
|
|
13
|
+
export async function getBrainPath(brain) {
|
|
14
|
+
if (!brain?.gitRepoUrl)
|
|
15
|
+
return config.DEFAULT_BRAIN_PATH;
|
|
16
|
+
const brainDir = join(config.BRAINS_PATH, brain.brainId);
|
|
17
|
+
if (!existsSync(brainDir)) {
|
|
18
|
+
logger.info(`Cloning brain ${brain.brainId} from ${brain.gitRepoUrl} (branch: ${brain.branch})...`);
|
|
19
|
+
await execAsync(`git clone --recurse-submodules ${brain.gitRepoUrl} ${brainDir}`);
|
|
20
|
+
// Ensure the desired branch exists (handles empty repos and missing branches)
|
|
21
|
+
const { stdout: currentBranch } = await execAsync('git branch --show-current', { cwd: brainDir });
|
|
22
|
+
if (currentBranch.trim() !== brain.branch) {
|
|
23
|
+
// Check if the branch exists on remote
|
|
24
|
+
const { stdout: remoteBranches } = await execAsync('git branch -r', { cwd: brainDir });
|
|
25
|
+
if (remoteBranches.includes(`origin/${brain.branch}`)) {
|
|
26
|
+
await execAsync(`git checkout ${brain.branch}`, { cwd: brainDir });
|
|
27
|
+
}
|
|
28
|
+
else {
|
|
29
|
+
await execAsync(`git checkout -b ${brain.branch}`, { cwd: brainDir });
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
logger.info(`Brain ${brain.brainId} cloned successfully`);
|
|
33
|
+
}
|
|
34
|
+
else if (config.BRAIN_AUTO_UPDATE) {
|
|
35
|
+
await updateBrain(brainDir, brain.brainId, brain.branch);
|
|
36
|
+
}
|
|
37
|
+
return brainDir;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Update a brain repo (respects cooldown)
|
|
41
|
+
*/
|
|
42
|
+
async function updateBrain(brainDir, brainId, branch) {
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const lastUpdate = lastUpdateTimes.get(brainId) || 0;
|
|
45
|
+
if (now - lastUpdate < config.BRAIN_UPDATE_COOLDOWN_MS) {
|
|
46
|
+
logger.debug(`Brain ${brainId} update skipped - cooldown active (${Math.round((config.BRAIN_UPDATE_COOLDOWN_MS - (now - lastUpdate)) / 1000)}s remaining)`);
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
logger.info(`Updating brain ${brainId} at ${brainDir}...`);
|
|
51
|
+
await execAsync(`git fetch origin ${branch} && git reset --hard origin/${branch} && git submodule update --init --recursive`, {
|
|
52
|
+
cwd: brainDir,
|
|
53
|
+
});
|
|
54
|
+
lastUpdateTimes.set(brainId, Date.now());
|
|
55
|
+
logger.info(`Brain ${brainId} updated successfully`);
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
59
|
+
logger.error(`Failed to update brain ${brainId}: ${errorMessage}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Ensure base brain repo is cloned and up to date.
|
|
64
|
+
* Uses same cooldown pattern as channel brains.
|
|
65
|
+
*/
|
|
66
|
+
export async function ensureBaseBrain() {
|
|
67
|
+
const brainDir = config.BASE_BRAIN_PATH;
|
|
68
|
+
if (!existsSync(brainDir)) {
|
|
69
|
+
logger.info(`Cloning base brain from ${config.BASE_BRAIN_REPO} (branch: ${config.BASE_BRAIN_BRANCH})...`);
|
|
70
|
+
await execAsync(`git clone --recurse-submodules --branch ${config.BASE_BRAIN_BRANCH} ${config.BASE_BRAIN_REPO} ${brainDir}`);
|
|
71
|
+
lastUpdateTimes.set('__base_brain__', Date.now());
|
|
72
|
+
logger.info('Base brain cloned successfully');
|
|
73
|
+
}
|
|
74
|
+
else if (config.BRAIN_AUTO_UPDATE) {
|
|
75
|
+
await updateBrain(brainDir, '__base_brain__', config.BASE_BRAIN_BRANCH);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Get the path to the base brain directory.
|
|
80
|
+
*/
|
|
81
|
+
export function getBaseBrainPath() {
|
|
82
|
+
return config.BASE_BRAIN_PATH;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Commit and push any changes in a brain repo.
|
|
86
|
+
* Silently skips if there are no changes or no remote.
|
|
87
|
+
*/
|
|
88
|
+
export async function pushBrainChanges(brainDir, brainId) {
|
|
89
|
+
try {
|
|
90
|
+
// Check if this is a git repo with a remote
|
|
91
|
+
try {
|
|
92
|
+
await execAsync('git remote get-url origin', { cwd: brainDir });
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
logger.debug(`Brain ${brainId} has no git remote, skipping push`);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
// Stage all changes
|
|
99
|
+
await execAsync('git add -A', { cwd: brainDir });
|
|
100
|
+
// Check if there are staged changes
|
|
101
|
+
try {
|
|
102
|
+
await execAsync('git diff --cached --quiet', { cwd: brainDir });
|
|
103
|
+
// If the command succeeds (exit 0), there are no changes
|
|
104
|
+
logger.debug(`Brain ${brainId} has no changes to push`);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
catch {
|
|
108
|
+
// Exit code 1 means there are changes — continue
|
|
109
|
+
}
|
|
110
|
+
await execAsync('git commit -m "auto: session update"', { cwd: brainDir });
|
|
111
|
+
await execAsync('git push', { cwd: brainDir });
|
|
112
|
+
logger.info(`Brain ${brainId} changes pushed successfully`);
|
|
113
|
+
}
|
|
114
|
+
catch (error) {
|
|
115
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
116
|
+
logger.warn(`Failed to push brain ${brainId} changes: ${errorMessage}`);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Ensure base paths exist
|
|
121
|
+
*/
|
|
122
|
+
export async function ensureDirectories() {
|
|
123
|
+
const { mkdirSync } = await import('fs');
|
|
124
|
+
if (!existsSync(config.BRAINS_PATH)) {
|
|
125
|
+
mkdirSync(config.BRAINS_PATH, { recursive: true });
|
|
126
|
+
}
|
|
127
|
+
if (!existsSync(config.DEFAULT_BRAIN_PATH)) {
|
|
128
|
+
mkdirSync(config.DEFAULT_BRAIN_PATH, { recursive: true });
|
|
129
|
+
}
|
|
130
|
+
}
|
package/dist/claude-spawner.d.ts
CHANGED
|
@@ -6,6 +6,6 @@ export interface SpawnResult {
|
|
|
6
6
|
exitCode: number | null;
|
|
7
7
|
error?: string;
|
|
8
8
|
}
|
|
9
|
-
export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string): Promise<SpawnResult>;
|
|
9
|
+
export declare function spawnClaudeCode(msg: TeamVibeQueueMessage, sessionLog: SessionLogger, cwd: string, sessionId?: string, isFirstMessage?: boolean, lastMessageTs?: string, onMessageSent?: () => void): Promise<SpawnResult>;
|
|
10
10
|
export declare function getActiveProcessCount(): number;
|
|
11
11
|
export declare function isAtCapacity(): boolean;
|
package/dist/claude-spawner.js
CHANGED
|
@@ -1,11 +1,9 @@
|
|
|
1
1
|
import { spawn } from 'child_process';
|
|
2
|
-
import { join
|
|
3
|
-
import { fileURLToPath } from 'url';
|
|
2
|
+
import { join } from 'path';
|
|
4
3
|
import { WebClient } from '@slack/web-api';
|
|
5
4
|
import { config } from './config.js';
|
|
6
5
|
import { logger } from './logger.js';
|
|
7
|
-
|
|
8
|
-
const SLACK_TOOL_PATH = join(__dirname, 'scripts/slack-tool.js');
|
|
6
|
+
import { getBaseBrainPath } from './brain-manager.js';
|
|
9
7
|
// Per-token Slack client cache for thread context
|
|
10
8
|
const slackClients = new Map();
|
|
11
9
|
function getSlackClient(botToken) {
|
|
@@ -42,62 +40,8 @@ async function countNewThreadMessages(botToken, channel, threadTs, sinceTs) {
|
|
|
42
40
|
return 0;
|
|
43
41
|
}
|
|
44
42
|
}
|
|
45
|
-
function
|
|
46
|
-
let prompt =
|
|
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
|
-
node $SLACK_TOOL_PATH send_message "Your message here"
|
|
54
|
-
\`\`\`
|
|
55
|
-
|
|
56
|
-
**Add emoji reaction:**
|
|
57
|
-
\`\`\`bash
|
|
58
|
-
node $SLACK_TOOL_PATH add_reaction emoji_name
|
|
59
|
-
\`\`\`
|
|
60
|
-
|
|
61
|
-
**Remove emoji reaction:**
|
|
62
|
-
\`\`\`bash
|
|
63
|
-
node $SLACK_TOOL_PATH remove_reaction emoji_name
|
|
64
|
-
\`\`\`
|
|
65
|
-
|
|
66
|
-
**Read thread history:**
|
|
67
|
-
\`\`\`bash
|
|
68
|
-
node $SLACK_TOOL_PATH read_thread [limit]
|
|
69
|
-
\`\`\`
|
|
70
|
-
|
|
71
|
-
**Upload code/text snippet:**
|
|
72
|
-
\`\`\`bash
|
|
73
|
-
node $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';
|
|
43
|
+
function buildPrompt(msg, resumeHint) {
|
|
44
|
+
let prompt = '## Incoming Slack Message\n\n';
|
|
101
45
|
prompt += `**From:** ${msg.sender.name}`;
|
|
102
46
|
if (msg.sender.id && msg.sender.id !== msg.sender.name) {
|
|
103
47
|
prompt += ` (${msg.sender.id})`;
|
|
@@ -123,7 +67,7 @@ function buildPrompt(msg, systemPrompt, resumeHint) {
|
|
|
123
67
|
if (resumeHint) {
|
|
124
68
|
prompt += `\n\n${resumeHint}`;
|
|
125
69
|
}
|
|
126
|
-
prompt +=
|
|
70
|
+
prompt += `\n\n---\n\nRespond to this user NOW using the send_message tool.`;
|
|
127
71
|
return prompt;
|
|
128
72
|
}
|
|
129
73
|
function truncateOutput(output, maxLen = 200) {
|
|
@@ -182,7 +126,7 @@ function formatStreamEvent(event) {
|
|
|
182
126
|
return null;
|
|
183
127
|
}
|
|
184
128
|
}
|
|
185
|
-
async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs) {
|
|
129
|
+
async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent) {
|
|
186
130
|
const slackContext = msg.response_context.slack;
|
|
187
131
|
const isCronMessage = msg.source === 'cron';
|
|
188
132
|
if (!slackContext && !isCronMessage) {
|
|
@@ -200,8 +144,14 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
200
144
|
}
|
|
201
145
|
}
|
|
202
146
|
return new Promise((resolve) => {
|
|
203
|
-
const
|
|
204
|
-
|
|
147
|
+
const prompt = buildPrompt(msg, resumeHint);
|
|
148
|
+
// MCP server config passed via --mcp-config flag (no temp files needed)
|
|
149
|
+
const mcpServerPath = join(getBaseBrainPath(), 'mcp', 'slack.mjs');
|
|
150
|
+
const mcpConfig = JSON.stringify({
|
|
151
|
+
mcpServers: {
|
|
152
|
+
slack: { command: 'node', args: [mcpServerPath] },
|
|
153
|
+
},
|
|
154
|
+
});
|
|
205
155
|
const args = [
|
|
206
156
|
'--print',
|
|
207
157
|
'--verbose',
|
|
@@ -209,8 +159,10 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
209
159
|
'stream-json',
|
|
210
160
|
'--max-turns',
|
|
211
161
|
'50',
|
|
162
|
+
'--mcp-config',
|
|
163
|
+
mcpConfig,
|
|
212
164
|
'--allowedTools',
|
|
213
|
-
'Bash,Read,WebFetch',
|
|
165
|
+
'Bash,Read,Write,Edit,WebFetch,mcp__slack__send_message,mcp__slack__add_reaction,mcp__slack__remove_reaction,mcp__slack__read_thread,mcp__slack__read_channel,mcp__slack__upload_snippet,mcp__slack__download_file,mcp__slack__upload_file,mcp__slack__set_status',
|
|
214
166
|
];
|
|
215
167
|
// Handle session continuity
|
|
216
168
|
if (sessionId) {
|
|
@@ -225,14 +177,22 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
225
177
|
sessionLog.info(`Working directory: ${cwd}`);
|
|
226
178
|
sessionLog.info(`Prompt length: ${prompt.length} chars`);
|
|
227
179
|
sessionLog.logPrompt(prompt);
|
|
180
|
+
// Prepend persistent bin dir to PATH if configured
|
|
181
|
+
const envPath = config.PERSISTENT_STORAGE_PATH
|
|
182
|
+
? `${config.PERSISTENT_STORAGE_PATH}/bin:${process.env['PATH'] || ''}`
|
|
183
|
+
: process.env['PATH'];
|
|
228
184
|
const proc = spawn(config.CLAUDE_CLI_PATH, args, {
|
|
229
185
|
cwd,
|
|
230
186
|
env: {
|
|
231
187
|
...process.env,
|
|
188
|
+
CLAUDECODE: undefined,
|
|
189
|
+
PATH: envPath,
|
|
232
190
|
CI: 'true',
|
|
191
|
+
CLAUDE_CONFIG_DIR: getBaseBrainPath(),
|
|
233
192
|
SLACK_BOT_TOKEN: msg.teamvibe.botToken,
|
|
234
|
-
|
|
235
|
-
|
|
193
|
+
...(config.PERSISTENT_STORAGE_PATH && {
|
|
194
|
+
PERSISTENT_STORAGE_PATH: config.PERSISTENT_STORAGE_PATH,
|
|
195
|
+
}),
|
|
236
196
|
...(slackContext && {
|
|
237
197
|
SLACK_CHANNEL: slackContext.channel,
|
|
238
198
|
SLACK_THREAD_TS: slackContext.thread_ts,
|
|
@@ -265,6 +225,12 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
265
225
|
if (formatted) {
|
|
266
226
|
sessionLog.claude('stdout', formatted);
|
|
267
227
|
}
|
|
228
|
+
// Detect when Claude sends a Slack message and clear typing indicator
|
|
229
|
+
if (onMessageSent &&
|
|
230
|
+
event.type === 'tool_use' &&
|
|
231
|
+
event.name === 'mcp__slack__send_message') {
|
|
232
|
+
onMessageSent();
|
|
233
|
+
}
|
|
268
234
|
}
|
|
269
235
|
catch {
|
|
270
236
|
const truncated = line.length > 500 ? line.slice(0, 500) + '...' : line;
|
|
@@ -307,12 +273,12 @@ async function runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = t
|
|
|
307
273
|
});
|
|
308
274
|
});
|
|
309
275
|
}
|
|
310
|
-
export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs) {
|
|
311
|
-
const result = await runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage, lastMessageTs);
|
|
276
|
+
export async function spawnClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage = true, lastMessageTs, onMessageSent) {
|
|
277
|
+
const result = await runClaudeCode(msg, sessionLog, cwd, sessionId, isFirstMessage, lastMessageTs, onMessageSent);
|
|
312
278
|
// If --resume failed, retry as a fresh session (session files may have been lost on container restart)
|
|
313
279
|
if (!result.success && sessionId && !isFirstMessage) {
|
|
314
280
|
sessionLog.info('Resume failed, retrying as fresh session (session files may have been lost)');
|
|
315
|
-
return runClaudeCode(msg, sessionLog, cwd, sessionId, true, lastMessageTs);
|
|
281
|
+
return runClaudeCode(msg, sessionLog, cwd, sessionId, true, lastMessageTs, onMessageSent);
|
|
316
282
|
}
|
|
317
283
|
return result;
|
|
318
284
|
}
|
package/dist/config.d.ts
CHANGED
|
@@ -12,11 +12,15 @@ declare const configSchema: z.ZodObject<{
|
|
|
12
12
|
CLAUDE_TIMEOUT_MS: z.ZodDefault<z.ZodNumber>;
|
|
13
13
|
STALE_LOCK_TIMEOUT_MS: z.ZodDefault<z.ZodNumber>;
|
|
14
14
|
TEAMVIBE_DATA_DIR: z.ZodDefault<z.ZodString>;
|
|
15
|
-
|
|
16
|
-
|
|
15
|
+
BRAINS_PATH: z.ZodDefault<z.ZodString>;
|
|
16
|
+
DEFAULT_BRAIN_PATH: z.ZodDefault<z.ZodString>;
|
|
17
17
|
CLAUDE_CLI_PATH: z.ZodDefault<z.ZodString>;
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
PERSISTENT_STORAGE_PATH: z.ZodDefault<z.ZodString>;
|
|
19
|
+
BASE_BRAIN_REPO: z.ZodDefault<z.ZodString>;
|
|
20
|
+
BASE_BRAIN_BRANCH: z.ZodDefault<z.ZodString>;
|
|
21
|
+
BASE_BRAIN_PATH: z.ZodDefault<z.ZodString>;
|
|
22
|
+
BRAIN_AUTO_UPDATE: z.ZodEffects<z.ZodDefault<z.ZodString>, boolean, string | undefined>;
|
|
23
|
+
BRAIN_UPDATE_COOLDOWN_MS: z.ZodDefault<z.ZodNumber>;
|
|
20
24
|
}, "strip", z.ZodTypeAny, {
|
|
21
25
|
AWS_REGION: string;
|
|
22
26
|
TEAMVIBE_API_URL: string;
|
|
@@ -27,11 +31,15 @@ declare const configSchema: z.ZodObject<{
|
|
|
27
31
|
CLAUDE_TIMEOUT_MS: number;
|
|
28
32
|
STALE_LOCK_TIMEOUT_MS: number;
|
|
29
33
|
TEAMVIBE_DATA_DIR: string;
|
|
30
|
-
|
|
31
|
-
|
|
34
|
+
BRAINS_PATH: string;
|
|
35
|
+
DEFAULT_BRAIN_PATH: string;
|
|
32
36
|
CLAUDE_CLI_PATH: string;
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
PERSISTENT_STORAGE_PATH: string;
|
|
38
|
+
BASE_BRAIN_REPO: string;
|
|
39
|
+
BASE_BRAIN_BRANCH: string;
|
|
40
|
+
BASE_BRAIN_PATH: string;
|
|
41
|
+
BRAIN_AUTO_UPDATE: boolean;
|
|
42
|
+
BRAIN_UPDATE_COOLDOWN_MS: number;
|
|
35
43
|
SQS_QUEUE_URL?: string | undefined;
|
|
36
44
|
SESSIONS_TABLE?: string | undefined;
|
|
37
45
|
TEAMVIBE_POLLER_TOKEN?: string | undefined;
|
|
@@ -48,11 +56,15 @@ declare const configSchema: z.ZodObject<{
|
|
|
48
56
|
CLAUDE_TIMEOUT_MS?: number | undefined;
|
|
49
57
|
STALE_LOCK_TIMEOUT_MS?: number | undefined;
|
|
50
58
|
TEAMVIBE_DATA_DIR?: string | undefined;
|
|
51
|
-
|
|
52
|
-
|
|
59
|
+
BRAINS_PATH?: string | undefined;
|
|
60
|
+
DEFAULT_BRAIN_PATH?: string | undefined;
|
|
53
61
|
CLAUDE_CLI_PATH?: string | undefined;
|
|
54
|
-
|
|
55
|
-
|
|
62
|
+
PERSISTENT_STORAGE_PATH?: string | undefined;
|
|
63
|
+
BASE_BRAIN_REPO?: string | undefined;
|
|
64
|
+
BASE_BRAIN_BRANCH?: string | undefined;
|
|
65
|
+
BASE_BRAIN_PATH?: string | undefined;
|
|
66
|
+
BRAIN_AUTO_UPDATE?: string | undefined;
|
|
67
|
+
BRAIN_UPDATE_COOLDOWN_MS?: number | undefined;
|
|
56
68
|
}>;
|
|
57
69
|
export type Config = z.infer<typeof configSchema>;
|
|
58
70
|
export declare function loadConfig(): Config;
|
|
@@ -66,11 +78,15 @@ export declare const config: {
|
|
|
66
78
|
CLAUDE_TIMEOUT_MS: number;
|
|
67
79
|
STALE_LOCK_TIMEOUT_MS: number;
|
|
68
80
|
TEAMVIBE_DATA_DIR: string;
|
|
69
|
-
|
|
70
|
-
|
|
81
|
+
BRAINS_PATH: string;
|
|
82
|
+
DEFAULT_BRAIN_PATH: string;
|
|
71
83
|
CLAUDE_CLI_PATH: string;
|
|
72
|
-
|
|
73
|
-
|
|
84
|
+
PERSISTENT_STORAGE_PATH: string;
|
|
85
|
+
BASE_BRAIN_REPO: string;
|
|
86
|
+
BASE_BRAIN_BRANCH: string;
|
|
87
|
+
BASE_BRAIN_PATH: string;
|
|
88
|
+
BRAIN_AUTO_UPDATE: boolean;
|
|
89
|
+
BRAIN_UPDATE_COOLDOWN_MS: number;
|
|
74
90
|
SQS_QUEUE_URL?: string | undefined;
|
|
75
91
|
SESSIONS_TABLE?: string | undefined;
|
|
76
92
|
TEAMVIBE_POLLER_TOKEN?: string | undefined;
|
package/dist/config.js
CHANGED
|
@@ -19,15 +19,20 @@ const configSchema = z.object({
|
|
|
19
19
|
STALE_LOCK_TIMEOUT_MS: z.coerce.number().default(2100000), // 35 minutes
|
|
20
20
|
// Paths
|
|
21
21
|
TEAMVIBE_DATA_DIR: z.string().default(DEFAULT_DATA_DIR),
|
|
22
|
-
|
|
23
|
-
|
|
22
|
+
BRAINS_PATH: z.string().default(''),
|
|
23
|
+
DEFAULT_BRAIN_PATH: z.string().default(''),
|
|
24
24
|
CLAUDE_CLI_PATH: z.string().default('claude'),
|
|
25
|
-
|
|
26
|
-
|
|
25
|
+
PERSISTENT_STORAGE_PATH: z.string().default(''),
|
|
26
|
+
// Base brain repo (user-scope config for Claude Code)
|
|
27
|
+
BASE_BRAIN_REPO: z.string().default('https://github.com/teamvibeai/poller-brain.git'),
|
|
28
|
+
BASE_BRAIN_BRANCH: z.string().default('main'),
|
|
29
|
+
BASE_BRAIN_PATH: z.string().default(''),
|
|
30
|
+
// Brain auto-update
|
|
31
|
+
BRAIN_AUTO_UPDATE: z
|
|
27
32
|
.string()
|
|
28
33
|
.default('true')
|
|
29
34
|
.transform((v) => v.toLowerCase() === 'true'),
|
|
30
|
-
|
|
35
|
+
BRAIN_UPDATE_COOLDOWN_MS: z.coerce.number().default(300000), // 5 minutes
|
|
31
36
|
});
|
|
32
37
|
export function loadConfig() {
|
|
33
38
|
const result = configSchema.safeParse(process.env);
|
|
@@ -47,8 +52,9 @@ export function loadConfig() {
|
|
|
47
52
|
}
|
|
48
53
|
// Derive paths from TEAMVIBE_DATA_DIR if not explicitly set
|
|
49
54
|
const dataDir = data.TEAMVIBE_DATA_DIR;
|
|
50
|
-
data.
|
|
51
|
-
data.
|
|
55
|
+
data.BRAINS_PATH = data.BRAINS_PATH || `${dataDir}/brains`;
|
|
56
|
+
data.DEFAULT_BRAIN_PATH = data.DEFAULT_BRAIN_PATH || `${dataDir}/workspace`;
|
|
57
|
+
data.BASE_BRAIN_PATH = data.BASE_BRAIN_PATH || `${dataDir}/base-brain`;
|
|
52
58
|
return data;
|
|
53
59
|
}
|
|
54
60
|
export const config = loadConfig();
|
package/dist/index.js
CHANGED
|
@@ -5,11 +5,11 @@ import { pollMessages, deleteMessage, extendVisibility, } from './sqs-poller.js'
|
|
|
5
5
|
import { spawnClaudeCode, isAtCapacity, getActiveProcessCount } from './claude-spawner.js';
|
|
6
6
|
import { sendSlackError, addReaction, getUserInfo, startTypingIndicator } from './slack-client.js';
|
|
7
7
|
import { acquireSessionLock, releaseSessionLock } from './session-store.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getBrainPath, ensureDirectories, ensureBaseBrain, pushBrainChanges } from './brain-manager.js';
|
|
9
9
|
import { initAuth, stopRefresh } from './auth-provider.js';
|
|
10
10
|
logger.info('TeamVibe Poller starting...');
|
|
11
11
|
logger.info(` Max concurrent: ${config.MAX_CONCURRENT_SESSIONS}`);
|
|
12
|
-
logger.info(`
|
|
12
|
+
logger.info(` Brains path: ${config.BRAINS_PATH}`);
|
|
13
13
|
// Track active message processing
|
|
14
14
|
const processingMessages = new Set();
|
|
15
15
|
// Per-thread completion signals
|
|
@@ -78,11 +78,11 @@ async function processMessage(received) {
|
|
|
78
78
|
}
|
|
79
79
|
sessionLog.info(`Processing message from ${queueMessage.sender.name} (${queueMessage.sender.id})`);
|
|
80
80
|
sessionLog.info(`Thread: ${threadId}`);
|
|
81
|
-
sessionLog.info(`
|
|
81
|
+
sessionLog.info(`Brain: ${queueMessage.teamvibe.brain?.brainId ?? 'none'}`);
|
|
82
82
|
sessionLog.info(`Type: ${queueMessage.type}`);
|
|
83
83
|
sessionLog.info(`Log file: ${sessionLog.getLogFile()}`);
|
|
84
|
-
// Get
|
|
85
|
-
const kbPath = await
|
|
84
|
+
// Get brain path for this channel
|
|
85
|
+
const kbPath = await getBrainPath(queueMessage.teamvibe.brain);
|
|
86
86
|
// Try to acquire session lock
|
|
87
87
|
let lockResult = await acquireSessionLock(threadId, kbPath);
|
|
88
88
|
if (!lockResult.success) {
|
|
@@ -115,10 +115,14 @@ async function processMessage(received) {
|
|
|
115
115
|
? startTypingIndicator(queueMessage)
|
|
116
116
|
: undefined;
|
|
117
117
|
try {
|
|
118
|
-
const result = await spawnClaudeCode(queueMessage, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts);
|
|
118
|
+
const result = await spawnClaudeCode(queueMessage, sessionLog, kbPath, session.session_id || undefined, isFirstMessage, session.last_message_ts, () => stopTyping?.());
|
|
119
119
|
stopTyping?.();
|
|
120
120
|
if (result.success) {
|
|
121
121
|
sessionLog.info('Claude Code completed successfully');
|
|
122
|
+
// Push any changes in the channel brain repo
|
|
123
|
+
if (queueMessage.teamvibe.brain?.brainId) {
|
|
124
|
+
await pushBrainChanges(kbPath, queueMessage.teamvibe.brain.brainId);
|
|
125
|
+
}
|
|
122
126
|
if (lockToken) {
|
|
123
127
|
const lastMessageTs = queueMessage.response_context.slack?.message_ts;
|
|
124
128
|
await releaseSessionLock(threadId, lockToken, 'idle', lastMessageTs);
|
|
@@ -230,6 +234,7 @@ async function main() {
|
|
|
230
234
|
logger.info(` Sessions table: ${config.SESSIONS_TABLE}`);
|
|
231
235
|
}
|
|
232
236
|
await ensureDirectories();
|
|
237
|
+
await ensureBaseBrain();
|
|
233
238
|
await pollLoop();
|
|
234
239
|
}
|
|
235
240
|
main().catch((error) => {
|
|
@@ -0,0 +1,22 @@
|
|
|
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)));
|
|
@@ -0,0 +1,16 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
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)));
|
|
@@ -0,0 +1,22 @@
|
|
|
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)));
|
|
@@ -0,0 +1,10 @@
|
|
|
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 {};
|
|
@@ -0,0 +1,31 @@
|
|
|
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)));
|
|
@@ -0,0 +1,19 @@
|
|
|
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)));
|
|
@@ -15,10 +15,33 @@ if (!BOT_TOKEN)
|
|
|
15
15
|
fail('SLACK_BOT_TOKEN not set');
|
|
16
16
|
const slack = new WebClient(BOT_TOKEN);
|
|
17
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
|
+
}
|
|
18
41
|
async function main() {
|
|
19
42
|
switch (command) {
|
|
20
43
|
case 'send_message': {
|
|
21
|
-
const text = args
|
|
44
|
+
const text = stripFlags(args);
|
|
22
45
|
if (!text)
|
|
23
46
|
fail('Message text required');
|
|
24
47
|
if (!CHANNEL)
|
|
@@ -0,0 +1,21 @@
|
|
|
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)));
|
package/dist/types.d.ts
CHANGED
|
@@ -29,16 +29,11 @@ export interface TeamVibeQueueMessage {
|
|
|
29
29
|
botId: string;
|
|
30
30
|
botToken: string;
|
|
31
31
|
channelId: string;
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
kbId: string;
|
|
38
|
-
gitRepoUrl: string;
|
|
39
|
-
branch: string;
|
|
40
|
-
claudePath: string;
|
|
41
|
-
};
|
|
32
|
+
brain?: {
|
|
33
|
+
brainId: string;
|
|
34
|
+
gitRepoUrl: string;
|
|
35
|
+
branch: string;
|
|
36
|
+
claudePath: string;
|
|
42
37
|
};
|
|
43
38
|
};
|
|
44
39
|
}
|