compass-agent 2.0.4
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/.github/workflows/publish.yml +24 -0
- package/README.md +294 -0
- package/bun.lock +326 -0
- package/manifest.yml +66 -0
- package/package.json +25 -0
- package/src/app.ts +786 -0
- package/src/db.ts +398 -0
- package/src/handlers/assistant.ts +332 -0
- package/src/handlers/cwd-modal.test.ts +188 -0
- package/src/handlers/cwd-modal.ts +63 -0
- package/src/handlers/setStatus.test.ts +118 -0
- package/src/handlers/stream.test.ts +137 -0
- package/src/handlers/stream.ts +908 -0
- package/src/lib/log.ts +16 -0
- package/src/lib/thread-context.ts +99 -0
- package/src/lib/worktree.ts +103 -0
- package/src/mcp/server.ts +286 -0
- package/src/types.ts +118 -0
- package/src/ui/blocks.ts +155 -0
- package/tests/blocks.test.ts +73 -0
- package/tests/db.test.ts +261 -0
- package/tests/thread-context.test.ts +183 -0
- package/tests/utils.test.ts +75 -0
- package/tsconfig.json +14 -0
package/src/lib/log.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function ts(): string {
|
|
2
|
+
return new Date().toISOString();
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
export function log(channel: string | null, ...args: unknown[]): void {
|
|
6
|
+
console.log(`${ts()} [${channel || "system"}]`, ...args);
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export function logErr(channel: string | null, ...args: unknown[]): void {
|
|
10
|
+
console.error(`${ts()} [${channel || "system"}]`, ...args);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function toSqliteDatetime(d: Date | string): string {
|
|
14
|
+
const date = typeof d === "string" ? new Date(d) : d;
|
|
15
|
+
return date.toISOString().replace("T", " ").replace(/\.\d+Z$/, "");
|
|
16
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Thread context extraction for bot mentions.
|
|
3
|
+
*
|
|
4
|
+
* When the bot is @mentioned in a thread, fetches the "gap" messages —
|
|
5
|
+
* messages between the bot's last reply and the current mention — so
|
|
6
|
+
* Claude has context of the conversation it missed.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { log, logErr } from "./log.ts";
|
|
10
|
+
|
|
11
|
+
const MAX_GAP_MESSAGES = 50;
|
|
12
|
+
|
|
13
|
+
export interface ThreadMessage {
|
|
14
|
+
user?: string;
|
|
15
|
+
text?: string;
|
|
16
|
+
ts: string;
|
|
17
|
+
bot_id?: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Pure logic: extract gap messages from a list of thread messages.
|
|
22
|
+
*
|
|
23
|
+
* Walks backwards from the end, collecting messages until it hits one
|
|
24
|
+
* from the bot (identified by `bot_id` field or `user === botUserId`).
|
|
25
|
+
* The current mention message is excluded. Returns up to 50 messages
|
|
26
|
+
* in chronological order.
|
|
27
|
+
*/
|
|
28
|
+
export function extractGapMessages(
|
|
29
|
+
messages: ThreadMessage[],
|
|
30
|
+
currentMessageTs: string,
|
|
31
|
+
botUserId: string | null,
|
|
32
|
+
): ThreadMessage[] {
|
|
33
|
+
// Filter out the current mention message
|
|
34
|
+
const filtered = messages.filter((m) => m.ts !== currentMessageTs);
|
|
35
|
+
|
|
36
|
+
const gap: ThreadMessage[] = [];
|
|
37
|
+
for (let i = filtered.length - 1; i >= 0; i--) {
|
|
38
|
+
const msg = filtered[i];
|
|
39
|
+
if (msg.bot_id || (botUserId && msg.user === botUserId)) break;
|
|
40
|
+
gap.unshift(msg);
|
|
41
|
+
if (gap.length >= MAX_GAP_MESSAGES) break;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return gap;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Format gap messages into a context string for Claude's prompt.
|
|
49
|
+
*/
|
|
50
|
+
export function formatGapMessages(gap: ThreadMessage[]): string {
|
|
51
|
+
if (gap.length === 0) return "";
|
|
52
|
+
|
|
53
|
+
const lines = gap.map((m) => `<@${m.user || "unknown"}>: ${m.text || ""}`);
|
|
54
|
+
return (
|
|
55
|
+
"[Thread context — messages since my last reply]\n" +
|
|
56
|
+
lines.join("\n") +
|
|
57
|
+
"\n\n---\n"
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Fetch thread replies from Slack and return formatted gap context.
|
|
63
|
+
*
|
|
64
|
+
* Returns an empty string if there are no gap messages or if the
|
|
65
|
+
* mention is not in a thread.
|
|
66
|
+
*/
|
|
67
|
+
export async function fetchThreadContext(
|
|
68
|
+
client: any,
|
|
69
|
+
channelId: string,
|
|
70
|
+
threadTs: string,
|
|
71
|
+
currentMessageTs: string,
|
|
72
|
+
botUserId: string | null,
|
|
73
|
+
): Promise<string> {
|
|
74
|
+
try {
|
|
75
|
+
log(channelId, `Fetching thread context: threadTs=${threadTs} currentTs=${currentMessageTs} botUserId=${botUserId}`);
|
|
76
|
+
|
|
77
|
+
const result = await client.conversations.replies({
|
|
78
|
+
channel: channelId,
|
|
79
|
+
ts: threadTs,
|
|
80
|
+
limit: 200,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const messages: ThreadMessage[] = result.messages || [];
|
|
84
|
+
log(channelId, `Thread replies: ${messages.length} message(s) fetched`);
|
|
85
|
+
|
|
86
|
+
const gap = extractGapMessages(messages, currentMessageTs, botUserId);
|
|
87
|
+
log(channelId, `Thread context: ${gap.length} gap message(s) found`);
|
|
88
|
+
|
|
89
|
+
const formatted = formatGapMessages(gap);
|
|
90
|
+
if (formatted) {
|
|
91
|
+
log(channelId, `Thread context injected (${formatted.length} chars)`);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return formatted;
|
|
95
|
+
} catch (err: any) {
|
|
96
|
+
logErr(channelId, `Failed to fetch thread context: ${err.message}`);
|
|
97
|
+
return "";
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { execFileSync } from "child_process";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import type { GitInfo, WorktreeResult } from "../types.ts";
|
|
5
|
+
|
|
6
|
+
export function detectGitRepo(cwd: string): GitInfo {
|
|
7
|
+
try {
|
|
8
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
9
|
+
cwd,
|
|
10
|
+
encoding: "utf-8",
|
|
11
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
12
|
+
}).trim();
|
|
13
|
+
return { isGit: true, repoRoot: root };
|
|
14
|
+
} catch {
|
|
15
|
+
return { isGit: false, repoRoot: null };
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function branchNameFromThread(threadTs: string): string {
|
|
20
|
+
return `slack/${threadTs.replace(".", "-")}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function createWorktree(repoRoot: string, threadTs: string, baseBranch?: string): WorktreeResult {
|
|
24
|
+
const branchName = branchNameFromThread(threadTs);
|
|
25
|
+
const treesDir = path.join(repoRoot, "trees");
|
|
26
|
+
const wtPath = path.join(treesDir, branchName.replace(/\//g, "-"));
|
|
27
|
+
|
|
28
|
+
if (!fs.existsSync(treesDir)) {
|
|
29
|
+
fs.mkdirSync(treesDir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!baseBranch) {
|
|
33
|
+
try {
|
|
34
|
+
baseBranch = execFileSync("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
35
|
+
cwd: repoRoot,
|
|
36
|
+
encoding: "utf-8",
|
|
37
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
38
|
+
}).trim();
|
|
39
|
+
} catch {
|
|
40
|
+
baseBranch = "main";
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
execFileSync("git", ["worktree", "add", "-b", branchName, wtPath, baseBranch], {
|
|
45
|
+
cwd: repoRoot,
|
|
46
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
execFileSync("git", ["worktree", "lock", wtPath, "--reason", `Slack thread: ${threadTs}`], {
|
|
51
|
+
cwd: repoRoot,
|
|
52
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
53
|
+
});
|
|
54
|
+
} catch {
|
|
55
|
+
// lock may fail if already locked, non-critical
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return { worktreePath: wtPath, branchName };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function removeWorktree(repoRoot: string, worktreePath: string, branchName: string): void {
|
|
62
|
+
try {
|
|
63
|
+
execFileSync("git", ["worktree", "unlock", worktreePath], {
|
|
64
|
+
cwd: repoRoot,
|
|
65
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
66
|
+
});
|
|
67
|
+
} catch {}
|
|
68
|
+
|
|
69
|
+
execFileSync("git", ["worktree", "remove", worktreePath, "--force"], {
|
|
70
|
+
cwd: repoRoot,
|
|
71
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
try {
|
|
75
|
+
execFileSync("git", ["branch", "-D", branchName], {
|
|
76
|
+
cwd: repoRoot,
|
|
77
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
78
|
+
});
|
|
79
|
+
} catch {}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function hasUncommittedChanges(worktreePath: string): boolean {
|
|
83
|
+
try {
|
|
84
|
+
const status = execFileSync("git", ["status", "--porcelain"], {
|
|
85
|
+
cwd: worktreePath,
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
88
|
+
});
|
|
89
|
+
return status.trim().length > 0;
|
|
90
|
+
} catch {
|
|
91
|
+
return true; // assume dirty if we can't check
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export function copyEnvFiles(sourceDir: string, targetDir: string): void {
|
|
96
|
+
const envFiles = [".env", ".env.local", ".env.development"];
|
|
97
|
+
for (const f of envFiles) {
|
|
98
|
+
const src = path.join(sourceDir, f);
|
|
99
|
+
if (fs.existsSync(src)) {
|
|
100
|
+
fs.copyFileSync(src, path.join(targetDir, f));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Compass MCP Server
|
|
5
|
+
*
|
|
6
|
+
* Exposes bot management tools (reminders, teachings, etc.) via MCP stdio transport.
|
|
7
|
+
* Can run standalone or be spawned by the Claude CLI as an MCP server.
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* bun src/mcp/server.ts # stdio transport (for Claude CLI)
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
14
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
import { CronExpressionParser } from "cron-parser";
|
|
17
|
+
import { toSqliteDatetime } from "../lib/log.ts";
|
|
18
|
+
|
|
19
|
+
// ── Database ────────────────────────────────────────────────
|
|
20
|
+
import {
|
|
21
|
+
addReminder,
|
|
22
|
+
getDueReminders,
|
|
23
|
+
updateNextTrigger,
|
|
24
|
+
deactivateReminder,
|
|
25
|
+
getActiveReminders,
|
|
26
|
+
addTeaching,
|
|
27
|
+
getTeachings,
|
|
28
|
+
removeTeaching,
|
|
29
|
+
getTeachingCount,
|
|
30
|
+
getChannelDefault,
|
|
31
|
+
setChannelDefault,
|
|
32
|
+
} from "../db.ts";
|
|
33
|
+
|
|
34
|
+
// Context defaults from env (injected by stream-handler when spawning Claude CLI)
|
|
35
|
+
const DEFAULT_CHANNEL_ID = process.env.SLACK_CHANNEL_ID || null;
|
|
36
|
+
const DEFAULT_USER_ID = process.env.SLACK_USER_ID || null;
|
|
37
|
+
const DEFAULT_BOT_USER_ID = process.env.SLACK_BOT_USER_ID || null;
|
|
38
|
+
|
|
39
|
+
// ── Server ──────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
const server = new McpServer({
|
|
42
|
+
name: "compass",
|
|
43
|
+
version: "1.0.0",
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ── Reminder tools ──────────────────────────────────────────
|
|
47
|
+
|
|
48
|
+
server.registerTool(
|
|
49
|
+
"claude_bot_create_reminder",
|
|
50
|
+
{
|
|
51
|
+
description:
|
|
52
|
+
"Create a scheduled reminder. For recurring reminders, provide a 5-field cron expression. For one-time reminders, provide an ISO 8601 datetime. The reminder will trigger Claude in the specified Slack channel at the scheduled time.",
|
|
53
|
+
inputSchema: {
|
|
54
|
+
channel_id: z.string().optional().describe("Slack channel ID (auto-detected from session context)"),
|
|
55
|
+
user_id: z.string().optional().describe("Slack user ID (auto-detected from session context)"),
|
|
56
|
+
bot_id: z.string().optional().describe("Bot user ID (auto-detected from session context)"),
|
|
57
|
+
content: z.string().describe("The task/action Claude should perform when the reminder fires"),
|
|
58
|
+
cron: z
|
|
59
|
+
.string()
|
|
60
|
+
.optional()
|
|
61
|
+
.describe("5-field cron expression for recurring reminders (e.g. '0 9 * * 1-5' for weekdays at 9am)"),
|
|
62
|
+
one_time_at: z
|
|
63
|
+
.string()
|
|
64
|
+
.optional()
|
|
65
|
+
.describe("ISO 8601 datetime for one-time reminders (e.g. '2026-02-17T15:00:00Z')"),
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
async (params: any) => {
|
|
69
|
+
const channel_id = params.channel_id || DEFAULT_CHANNEL_ID;
|
|
70
|
+
const user_id = params.user_id || DEFAULT_USER_ID;
|
|
71
|
+
const bot_id = params.bot_id || DEFAULT_BOT_USER_ID;
|
|
72
|
+
|
|
73
|
+
if (!channel_id || !user_id || !bot_id) {
|
|
74
|
+
return { content: [{ type: "text" as const, text: "Error: Missing channel_id, user_id, or bot_id and no session context available." }] };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const { content, cron, one_time_at } = params;
|
|
78
|
+
|
|
79
|
+
if (!cron && !one_time_at) {
|
|
80
|
+
return { content: [{ type: "text" as const, text: "Error: Provide either 'cron' (recurring) or 'one_time_at' (one-time)." }] };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
let nextTriggerAt: string;
|
|
84
|
+
let cronExpression: string | null = null;
|
|
85
|
+
let oneTime = 0;
|
|
86
|
+
|
|
87
|
+
if (cron) {
|
|
88
|
+
try {
|
|
89
|
+
const interval = CronExpressionParser.parse(cron);
|
|
90
|
+
nextTriggerAt = toSqliteDatetime(interval.next().toDate());
|
|
91
|
+
cronExpression = cron;
|
|
92
|
+
} catch (err: any) {
|
|
93
|
+
return { content: [{ type: "text" as const, text: `Error: Invalid cron expression "${cron}": ${err.message}` }] };
|
|
94
|
+
}
|
|
95
|
+
} else {
|
|
96
|
+
const triggerDate = new Date(one_time_at);
|
|
97
|
+
if (isNaN(triggerDate.getTime())) {
|
|
98
|
+
return { content: [{ type: "text" as const, text: `Error: Invalid datetime "${one_time_at}".` }] };
|
|
99
|
+
}
|
|
100
|
+
if (triggerDate <= new Date()) {
|
|
101
|
+
return { content: [{ type: "text" as const, text: "Error: That time is in the past. Specify a future time." }] };
|
|
102
|
+
}
|
|
103
|
+
nextTriggerAt = toSqliteDatetime(triggerDate);
|
|
104
|
+
oneTime = 1;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const originalInput = cron ? `[MCP] ${content} (cron: ${cron})` : `[MCP] ${content} (at: ${one_time_at})`;
|
|
108
|
+
addReminder(channel_id, user_id, bot_id, content, originalInput, cronExpression, oneTime, nextTriggerAt);
|
|
109
|
+
|
|
110
|
+
const scheduleDesc = cronExpression ? `recurring (cron: ${cronExpression})` : "one-time";
|
|
111
|
+
return {
|
|
112
|
+
content: [
|
|
113
|
+
{
|
|
114
|
+
type: "text" as const,
|
|
115
|
+
text: `Reminder created.\nContent: ${content}\nSchedule: ${scheduleDesc}\nNext trigger: ${nextTriggerAt}\nChannel: ${channel_id}`,
|
|
116
|
+
},
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
);
|
|
121
|
+
|
|
122
|
+
server.registerTool(
|
|
123
|
+
"claude_bot_list_reminders",
|
|
124
|
+
{
|
|
125
|
+
description: "List all active reminders for a user.",
|
|
126
|
+
inputSchema: {
|
|
127
|
+
user_id: z.string().optional().describe("Slack user ID (auto-detected from session context)"),
|
|
128
|
+
},
|
|
129
|
+
},
|
|
130
|
+
async (params: any) => {
|
|
131
|
+
const user_id = params.user_id || DEFAULT_USER_ID;
|
|
132
|
+
if (!user_id) {
|
|
133
|
+
return { content: [{ type: "text" as const, text: "Error: Missing user_id and no session context available." }] };
|
|
134
|
+
}
|
|
135
|
+
const reminders = getActiveReminders(user_id);
|
|
136
|
+
if (reminders.length === 0) {
|
|
137
|
+
return { content: [{ type: "text" as const, text: "No active reminders." }] };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const lines = reminders.map((r) => {
|
|
141
|
+
const schedule = r.cron_expression ? `cron: ${r.cron_expression}` : "one-time";
|
|
142
|
+
return `#${r.id} — ${r.content} (${schedule}, next: ${r.next_trigger_at}, channel: ${r.channel_id})`;
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
return { content: [{ type: "text" as const, text: `Active reminders:\n${lines.join("\n")}` }] };
|
|
146
|
+
}
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
server.registerTool(
|
|
150
|
+
"claude_bot_delete_reminder",
|
|
151
|
+
{
|
|
152
|
+
description: "Deactivate/delete a reminder by its ID.",
|
|
153
|
+
inputSchema: {
|
|
154
|
+
reminder_id: z.number().describe("The reminder ID to deactivate"),
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
async ({ reminder_id }: any) => {
|
|
158
|
+
deactivateReminder(reminder_id);
|
|
159
|
+
return { content: [{ type: "text" as const, text: `Reminder #${reminder_id} deactivated.` }] };
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
|
|
163
|
+
// ── Teaching tools ──────────────────────────────────────────
|
|
164
|
+
|
|
165
|
+
server.registerTool(
|
|
166
|
+
"claude_bot_add_teaching",
|
|
167
|
+
{
|
|
168
|
+
description:
|
|
169
|
+
"Add a team knowledge instruction. Teachings are injected into all future Claude sessions as system prompts, letting you customize bot behavior across the workspace.",
|
|
170
|
+
inputSchema: {
|
|
171
|
+
instruction: z.string().describe("The instruction/teaching to add"),
|
|
172
|
+
added_by: z.string().optional().describe("Slack user ID (auto-detected from session context)"),
|
|
173
|
+
workspace_id: z.string().optional().describe("Workspace ID (defaults to 'default')"),
|
|
174
|
+
},
|
|
175
|
+
},
|
|
176
|
+
async ({ instruction, added_by, workspace_id }: any) => {
|
|
177
|
+
const userId = added_by || DEFAULT_USER_ID;
|
|
178
|
+
if (!userId) {
|
|
179
|
+
return { content: [{ type: "text" as const, text: "Error: Missing added_by and no session context available." }] };
|
|
180
|
+
}
|
|
181
|
+
addTeaching(instruction, userId, workspace_id || "default");
|
|
182
|
+
const count = getTeachingCount(workspace_id || "default");
|
|
183
|
+
return {
|
|
184
|
+
content: [{ type: "text" as const, text: `Teaching added. Total active teachings: ${count.count}` }],
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
|
|
189
|
+
server.registerTool(
|
|
190
|
+
"claude_bot_list_teachings",
|
|
191
|
+
{
|
|
192
|
+
description: "List all active team knowledge instructions.",
|
|
193
|
+
inputSchema: {
|
|
194
|
+
workspace_id: z.string().optional().describe("Workspace ID (defaults to 'default')"),
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
async ({ workspace_id }: any) => {
|
|
198
|
+
const teachings = getTeachings(workspace_id || "default");
|
|
199
|
+
if (teachings.length === 0) {
|
|
200
|
+
return { content: [{ type: "text" as const, text: "No active teachings." }] };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const lines = teachings.map(
|
|
204
|
+
(t) => `#${t.id} — ${t.instruction} (by ${t.added_by}, ${t.created_at})`
|
|
205
|
+
);
|
|
206
|
+
return { content: [{ type: "text" as const, text: `Active teachings:\n${lines.join("\n")}` }] };
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
|
|
210
|
+
server.registerTool(
|
|
211
|
+
"claude_bot_remove_teaching",
|
|
212
|
+
{
|
|
213
|
+
description: "Remove a team knowledge instruction by its ID.",
|
|
214
|
+
inputSchema: {
|
|
215
|
+
teaching_id: z.number().describe("The teaching ID to remove"),
|
|
216
|
+
},
|
|
217
|
+
},
|
|
218
|
+
async ({ teaching_id }: any) => {
|
|
219
|
+
removeTeaching(teaching_id);
|
|
220
|
+
return { content: [{ type: "text" as const, text: `Teaching #${teaching_id} removed.` }] };
|
|
221
|
+
}
|
|
222
|
+
);
|
|
223
|
+
|
|
224
|
+
// ── Channel default CWD tools ───────────────────────────────
|
|
225
|
+
|
|
226
|
+
server.registerTool(
|
|
227
|
+
"claude_bot_get_channel_cwd",
|
|
228
|
+
{
|
|
229
|
+
description: "Get the default working directory for a Slack channel.",
|
|
230
|
+
inputSchema: {
|
|
231
|
+
channel_id: z.string().optional().describe("Slack channel ID (auto-detected from session context)"),
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
async (params: any) => {
|
|
235
|
+
const channel_id = params.channel_id || DEFAULT_CHANNEL_ID;
|
|
236
|
+
if (!channel_id) {
|
|
237
|
+
return { content: [{ type: "text" as const, text: "Error: Missing channel_id and no session context available." }] };
|
|
238
|
+
}
|
|
239
|
+
const result = getChannelDefault(channel_id);
|
|
240
|
+
if (!result) {
|
|
241
|
+
return { content: [{ type: "text" as const, text: `No default CWD set for channel ${channel_id}.` }] };
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
content: [
|
|
245
|
+
{
|
|
246
|
+
type: "text" as const,
|
|
247
|
+
text: `Channel ${channel_id} default CWD: ${result.cwd} (set by ${result.set_by}, updated ${result.updated_at})`,
|
|
248
|
+
},
|
|
249
|
+
],
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
server.registerTool(
|
|
255
|
+
"claude_bot_set_channel_cwd",
|
|
256
|
+
{
|
|
257
|
+
description: "Set the default working directory for a Slack channel.",
|
|
258
|
+
inputSchema: {
|
|
259
|
+
channel_id: z.string().optional().describe("Slack channel ID (auto-detected from session context)"),
|
|
260
|
+
cwd: z.string().describe("Absolute path to set as the default working directory"),
|
|
261
|
+
set_by: z.string().optional().describe("Slack user ID (auto-detected from session context)"),
|
|
262
|
+
},
|
|
263
|
+
},
|
|
264
|
+
async ({ channel_id, cwd, set_by }: any) => {
|
|
265
|
+
const resolvedChannel = channel_id || DEFAULT_CHANNEL_ID;
|
|
266
|
+
const resolvedUser = set_by || DEFAULT_USER_ID;
|
|
267
|
+
if (!resolvedChannel) {
|
|
268
|
+
return { content: [{ type: "text" as const, text: "Error: Missing channel_id and no session context available." }] };
|
|
269
|
+
}
|
|
270
|
+
setChannelDefault(resolvedChannel, cwd, resolvedUser);
|
|
271
|
+
return { content: [{ type: "text" as const, text: `Channel ${resolvedChannel} default CWD set to: ${cwd}` }] };
|
|
272
|
+
}
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// ── Start ───────────────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
async function main(): Promise<void> {
|
|
278
|
+
const transport = new StdioServerTransport();
|
|
279
|
+
await server.connect(transport);
|
|
280
|
+
console.error("compass MCP server running on stdio");
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
main().catch((err) => {
|
|
284
|
+
console.error("MCP server fatal error:", err);
|
|
285
|
+
process.exit(1);
|
|
286
|
+
});
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import type { ChildProcess } from "child_process";
|
|
2
|
+
|
|
3
|
+
// ── Database row types ──────────────────────────────────────
|
|
4
|
+
|
|
5
|
+
export interface SessionRow {
|
|
6
|
+
channel_id: string;
|
|
7
|
+
session_id: string;
|
|
8
|
+
persisted: number;
|
|
9
|
+
cwd: string | null;
|
|
10
|
+
user_id: string | null;
|
|
11
|
+
created_at: string;
|
|
12
|
+
updated_at: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface CwdHistoryRow {
|
|
16
|
+
path: string;
|
|
17
|
+
last_used: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ChannelDefaultRow {
|
|
21
|
+
channel_id: string;
|
|
22
|
+
cwd: string;
|
|
23
|
+
set_by: string | null;
|
|
24
|
+
updated_at: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface TeachingRow {
|
|
28
|
+
id: number;
|
|
29
|
+
instruction: string;
|
|
30
|
+
added_by: string;
|
|
31
|
+
workspace_id: string;
|
|
32
|
+
created_at: string;
|
|
33
|
+
active: number;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface ReminderRow {
|
|
37
|
+
id: number;
|
|
38
|
+
channel_id: string;
|
|
39
|
+
user_id: string;
|
|
40
|
+
bot_id: string;
|
|
41
|
+
content: string;
|
|
42
|
+
original_input: string;
|
|
43
|
+
cron_expression: string | null;
|
|
44
|
+
one_time: number;
|
|
45
|
+
next_trigger_at: string;
|
|
46
|
+
active: number;
|
|
47
|
+
created_at: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export interface UsageLogRow {
|
|
51
|
+
id: number;
|
|
52
|
+
session_key: string;
|
|
53
|
+
user_id: string;
|
|
54
|
+
model: string | null;
|
|
55
|
+
input_tokens: number;
|
|
56
|
+
output_tokens: number;
|
|
57
|
+
total_cost_usd: number;
|
|
58
|
+
duration_ms: number;
|
|
59
|
+
num_turns: number;
|
|
60
|
+
created_at: string;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export interface WorktreeRow {
|
|
64
|
+
session_key: string;
|
|
65
|
+
repo_path: string;
|
|
66
|
+
worktree_path: string;
|
|
67
|
+
branch_name: string;
|
|
68
|
+
locked: number;
|
|
69
|
+
created_at: string;
|
|
70
|
+
last_active_at: string;
|
|
71
|
+
cleaned_up: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export interface FeedbackRow {
|
|
75
|
+
id: number;
|
|
76
|
+
session_key: string;
|
|
77
|
+
user_id: string;
|
|
78
|
+
sentiment: string;
|
|
79
|
+
message_ts: string | null;
|
|
80
|
+
created_at: string;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export interface TeachingCountRow {
|
|
84
|
+
count: number;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ── Runtime types ───────────────────────────────────────────
|
|
88
|
+
|
|
89
|
+
export type ActiveProcessMap = Map<string, ChildProcess>;
|
|
90
|
+
|
|
91
|
+
export interface Ref<T> {
|
|
92
|
+
value: T;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface HandleClaudeStreamOpts {
|
|
96
|
+
channelId: string;
|
|
97
|
+
threadTs: string;
|
|
98
|
+
userText: string;
|
|
99
|
+
userId: string;
|
|
100
|
+
client: any;
|
|
101
|
+
spawnCwd: string;
|
|
102
|
+
isResume: boolean;
|
|
103
|
+
sessionId: string;
|
|
104
|
+
setStatus: (status: string | { status: string; loading_messages?: string[] }) => Promise<void>;
|
|
105
|
+
activeProcesses: ActiveProcessMap;
|
|
106
|
+
cachedTeamId: string | null;
|
|
107
|
+
botUserId: string | null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface GitInfo {
|
|
111
|
+
isGit: boolean;
|
|
112
|
+
repoRoot: string | null;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export interface WorktreeResult {
|
|
116
|
+
worktreePath: string;
|
|
117
|
+
branchName: string;
|
|
118
|
+
}
|