@co-syn/forest 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # @co-syn/forest
2
+
3
+ Bridge your Claude Code sessions with your team. Share context, sync plans, track tasks — one command to set up.
4
+
5
+ ## Quick start
6
+
7
+ ```bash
8
+ npx @co-syn/forest init --token forestpat_YOUR_TOKEN
9
+ ```
10
+
11
+ That's it. This registers the MCP server, configures the plan sync hook, and saves your credentials.
12
+
13
+ ## What it does
14
+
15
+ ### MCP Tools (Claude calls these)
16
+
17
+ | Tool | Description |
18
+ |------|-------------|
19
+ | `forest_sync` | Share a summary of your session with the team via Slack |
20
+ | `forest_tasks` | List active tasks from Forest |
21
+ | `forest_update` | Update a task's status or add notes |
22
+
23
+ ### Plan Sync Hook (automatic)
24
+
25
+ When you exit plan mode in Claude Code ("Accept Edits"), the plan markdown is automatically sent to Forest. From there, Forest can:
26
+ - Match the plan to active tasks
27
+ - Post to your team's Slack channel
28
+ - Update project context
29
+
30
+ ## Setup
31
+
32
+ ### 1. Get your token
33
+
34
+ Generate a personal API token from the Forest web app settings page.
35
+
36
+ ### 2. Run init
37
+
38
+ ```bash
39
+ npx @co-syn/forest init --token forestpat_YOUR_TOKEN
40
+ ```
41
+
42
+ For a custom backend URL:
43
+
44
+ ```bash
45
+ npx @co-syn/forest init --token forestpat_YOUR_TOKEN --api-url https://your-backend.example.com
46
+ ```
47
+
48
+ ### 3. Use it
49
+
50
+ In Claude Code, just ask naturally:
51
+
52
+ - "sync what we did to the team"
53
+ - "share this with @john on #engineering"
54
+ - "show me active tasks"
55
+ - "mark the auth task as done"
56
+
57
+ Plans sync automatically when you accept edits from plan mode — no action needed.
58
+
59
+ ## How it works
60
+
61
+ ```
62
+ npx @co-syn/forest init
63
+ ├── Saves token to ~/.config/forest/config.json
64
+ ├── Registers MCP server with Claude Code
65
+ └── Adds ExitPlanMode hook to ~/.claude/settings.json
66
+
67
+ During a session:
68
+ Claude Code ←─stdio──→ forest mcp (MCP server, local process)
69
+
70
+ HTTPS + Bearer token
71
+
72
+
73
+ Forest Backend (your org)
74
+ ├── Slack
75
+ ├── Tasks DB
76
+ └── Normalized Events
77
+
78
+ On "Accept Edits":
79
+ ExitPlanMode hook fires → forest hook plan-exit
80
+ → reads transcript → extracts plan markdown
81
+ → POST /mcp/plan to Forest backend
82
+ ```
83
+
84
+ ## Local development
85
+
86
+ ```bash
87
+ npx @co-syn/forest init --token forestpat_YOUR_TOKEN --api-url http://localhost:8787
88
+ ```
89
+
90
+ Or run the MCP server directly:
91
+
92
+ ```bash
93
+ FOREST_API_TOKEN=forestpat_... FOREST_API_URL=http://localhost:8787 node packages/forest-mcp/build/mcp.js
94
+ ```
95
+
96
+ ## CLI reference
97
+
98
+ ```
99
+ npx @co-syn/forest init --token <token> # Set up everything
100
+ npx @co-syn/forest mcp # Start MCP server (used by Claude Code)
101
+ npx @co-syn/forest hook plan-exit # Run plan-exit hook (used by Claude Code)
102
+ ```
package/build/cli.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forest CLI — setup tool for Forest + Claude Code integration.
4
+ *
5
+ * Usage:
6
+ * npx @co-syn/forest init --token forestpat_abc123
7
+ * npx @co-syn/forest init --token forestpat_abc123 --api-url https://api.example.com
8
+ */
9
+ export {};
package/build/cli.js ADDED
@@ -0,0 +1,150 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forest CLI — setup tool for Forest + Claude Code integration.
4
+ *
5
+ * Usage:
6
+ * npx @co-syn/forest init --token forestpat_abc123
7
+ * npx @co-syn/forest init --token forestpat_abc123 --api-url https://api.example.com
8
+ */
9
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { join } from "path";
12
+ import { execSync } from "child_process";
13
+ import { writeConfigFile, resolveConfig } from "./lib/config.js";
14
+ import { forestApi } from "./lib/api.js";
15
+ function parseArgs(argv) {
16
+ const args = {};
17
+ for (let i = 0; i < argv.length; i++) {
18
+ if (argv[i].startsWith("--") && i + 1 < argv.length) {
19
+ const key = argv[i].slice(2);
20
+ args[key] = argv[i + 1];
21
+ i++;
22
+ }
23
+ }
24
+ return args;
25
+ }
26
+ function readClaudeSettings() {
27
+ const settingsPath = join(homedir(), ".claude", "settings.json");
28
+ try {
29
+ return JSON.parse(readFileSync(settingsPath, "utf-8"));
30
+ }
31
+ catch {
32
+ return {};
33
+ }
34
+ }
35
+ function writeClaudeSettings(settings) {
36
+ const settingsPath = join(homedir(), ".claude", "settings.json");
37
+ mkdirSync(join(homedir(), ".claude"), { recursive: true });
38
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + "\n", "utf-8");
39
+ }
40
+ function mergeHookConfig(settings) {
41
+ const hooks = (settings.hooks ?? {});
42
+ const preToolUse = (hooks.PreToolUse ?? []);
43
+ // Check if we already have an ExitPlanMode hook
44
+ const existing = preToolUse.find((h) => h.matcher === "ExitPlanMode");
45
+ if (existing) {
46
+ console.log(" Hook already configured for ExitPlanMode — skipping");
47
+ return;
48
+ }
49
+ preToolUse.push({
50
+ matcher: "ExitPlanMode",
51
+ hooks: [
52
+ {
53
+ type: "command",
54
+ command: "npx @co-syn/forest hook plan-exit",
55
+ async: true,
56
+ },
57
+ ],
58
+ });
59
+ hooks.PreToolUse = preToolUse;
60
+ settings.hooks = hooks;
61
+ }
62
+ async function init(args) {
63
+ const token = args.token;
64
+ const apiUrl = args["api-url"] ?? "https://api.forestworks.ai";
65
+ if (!token) {
66
+ console.error("Usage: npx @co-syn/forest init --token <forestpat_...>");
67
+ console.error("");
68
+ console.error("Get your token from the Forest web app settings page.");
69
+ process.exit(1);
70
+ }
71
+ if (!token.startsWith("forestpat_")) {
72
+ console.error("Invalid token format. Tokens start with 'forestpat_'.");
73
+ process.exit(1);
74
+ }
75
+ console.log("Setting up Forest for Claude Code...\n");
76
+ // 1. Write config file
77
+ console.log("1. Saving credentials...");
78
+ writeConfigFile({ token, apiUrl });
79
+ console.log(" Saved to ~/.config/forest/config.json\n");
80
+ // 2. Validate token by calling the tasks endpoint
81
+ console.log("2. Validating token...");
82
+ try {
83
+ const config = resolveConfig();
84
+ await forestApi(config, "/tasks");
85
+ console.log(" Token is valid.\n");
86
+ }
87
+ catch (err) {
88
+ console.error(` Token validation failed: ${err instanceof Error ? err.message : String(err)}`);
89
+ console.error(" The token was saved but may not work. Check your token and API URL.");
90
+ console.log("");
91
+ }
92
+ // 3. Register MCP server with Claude Code
93
+ console.log("3. Registering MCP server...");
94
+ try {
95
+ execSync(`claude mcp add forest -s user -- npx @co-syn/forest mcp`, { stdio: "pipe" });
96
+ console.log(" MCP server registered.\n");
97
+ }
98
+ catch {
99
+ console.log(" Could not auto-register. Run manually:");
100
+ console.log(" claude mcp add forest -s user -- npx @co-syn/forest mcp\n");
101
+ }
102
+ // 4. Add hook config
103
+ console.log("4. Configuring plan sync hook...");
104
+ const settings = readClaudeSettings();
105
+ mergeHookConfig(settings);
106
+ writeClaudeSettings(settings);
107
+ console.log(" Hook added to ~/.claude/settings.json\n");
108
+ console.log("Done! Forest is ready.\n");
109
+ console.log("What happens now:");
110
+ console.log(" - Claude Code can share session context via forest_sync");
111
+ console.log(" - Plans auto-sync to Forest when you accept edits");
112
+ console.log(" - View and update tasks with forest_tasks / forest_update");
113
+ }
114
+ // --- Entrypoint ---
115
+ const subcommand = process.argv[2];
116
+ const args = parseArgs(process.argv.slice(3));
117
+ switch (subcommand) {
118
+ case "init":
119
+ init(args).catch((err) => {
120
+ console.error("Setup failed:", err);
121
+ process.exit(1);
122
+ });
123
+ break;
124
+ case "mcp":
125
+ // Forward to MCP server — dynamic import to avoid loading MCP deps for other commands
126
+ import("./mcp.js").catch((err) => {
127
+ console.error("Failed to start MCP server:", err);
128
+ process.exit(1);
129
+ });
130
+ break;
131
+ case "hook":
132
+ // Forward to hook handler
133
+ // Re-invoke with correct argv position
134
+ process.argv = [process.argv[0], process.argv[1], ...process.argv.slice(3)];
135
+ import("./hook.js").catch((err) => {
136
+ console.error("Hook error:", err);
137
+ process.exit(0); // don't block
138
+ });
139
+ break;
140
+ default:
141
+ console.log("Forest CLI — Claude Code integration\n");
142
+ console.log("Commands:");
143
+ console.log(" init --token <token> Set up Forest for Claude Code");
144
+ console.log(" mcp Start the MCP server (used by Claude Code)");
145
+ console.log(" hook <name> Run a hook handler (used by Claude Code)");
146
+ console.log("");
147
+ console.log("Get started:");
148
+ console.log(" npx @co-syn/forest init --token forestpat_YOUR_TOKEN");
149
+ break;
150
+ }
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forest hook handler — runs when a Claude Code hook fires.
4
+ *
5
+ * Usage:
6
+ * forest-hook plan-exit (called by PreToolUse hook on ExitPlanMode)
7
+ *
8
+ * Reads the hook event JSON from stdin, extracts the plan from the
9
+ * conversation transcript, and POSTs it to the Forest backend.
10
+ */
11
+ export {};
package/build/hook.js ADDED
@@ -0,0 +1,142 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forest hook handler — runs when a Claude Code hook fires.
4
+ *
5
+ * Usage:
6
+ * forest-hook plan-exit (called by PreToolUse hook on ExitPlanMode)
7
+ *
8
+ * Reads the hook event JSON from stdin, extracts the plan from the
9
+ * conversation transcript, and POSTs it to the Forest backend.
10
+ */
11
+ import { readFileSync } from "fs";
12
+ import { resolveConfig } from "./lib/config.js";
13
+ import { forestApi } from "./lib/api.js";
14
+ function extractText(content) {
15
+ if (!content)
16
+ return "";
17
+ if (typeof content === "string")
18
+ return content;
19
+ return content
20
+ .filter((block) => block.type === "text" && block.text)
21
+ .map((block) => block.text)
22
+ .join("\n");
23
+ }
24
+ function extractPlanFromTranscript(transcriptPath) {
25
+ let raw;
26
+ try {
27
+ raw = readFileSync(transcriptPath, "utf-8");
28
+ }
29
+ catch {
30
+ return null;
31
+ }
32
+ const lines = raw.split("\n").filter(Boolean);
33
+ const entries = [];
34
+ for (const line of lines) {
35
+ try {
36
+ entries.push(JSON.parse(line));
37
+ }
38
+ catch {
39
+ // skip malformed lines
40
+ }
41
+ }
42
+ if (entries.length === 0)
43
+ return null;
44
+ // Find the last EnterPlanMode — everything after that is the plan
45
+ let planStartIndex = -1;
46
+ for (let i = entries.length - 1; i >= 0; i--) {
47
+ const e = entries[i];
48
+ const isEnterPlan = e.tool_name === "EnterPlanMode" ||
49
+ e.name === "EnterPlanMode" ||
50
+ (e.type === "tool_use" && e.name === "EnterPlanMode") ||
51
+ (e.message?.content &&
52
+ Array.isArray(e.message.content) &&
53
+ e.message.content.some((block) => block.type === "tool_use" && block.name === "EnterPlanMode"));
54
+ if (isEnterPlan) {
55
+ planStartIndex = i;
56
+ break;
57
+ }
58
+ }
59
+ if (planStartIndex === -1) {
60
+ // No EnterPlanMode found — grab the last few assistant messages as fallback
61
+ const lastAssistant = entries
62
+ .filter((e) => e.role === "assistant" ||
63
+ e.message?.role === "assistant" ||
64
+ e.type === "assistant")
65
+ .slice(-5);
66
+ if (lastAssistant.length === 0)
67
+ return null;
68
+ return lastAssistant
69
+ .map((e) => extractText(e.message?.content ?? e.content))
70
+ .filter(Boolean)
71
+ .join("\n\n");
72
+ }
73
+ // Collect assistant text content from planStartIndex onward
74
+ const planParts = [];
75
+ for (let i = planStartIndex + 1; i < entries.length; i++) {
76
+ const e = entries[i];
77
+ const isAssistant = e.role === "assistant" ||
78
+ e.message?.role === "assistant" ||
79
+ e.type === "assistant";
80
+ if (!isAssistant)
81
+ continue;
82
+ const text = extractText(e.message?.content ?? e.content);
83
+ if (text.trim())
84
+ planParts.push(text);
85
+ }
86
+ return planParts.length > 0 ? planParts.join("\n\n") : null;
87
+ }
88
+ async function handlePlanExit() {
89
+ const config = resolveConfig();
90
+ if (!config) {
91
+ process.stderr.write("Forest not configured. Run: npx @co-syn/forest init --token <token>\n");
92
+ process.exit(0); // exit 0 so we don't block the tool
93
+ }
94
+ // Read hook event from stdin
95
+ let eventJson;
96
+ try {
97
+ eventJson = readFileSync("/dev/stdin", "utf-8");
98
+ }
99
+ catch {
100
+ // stdin not available — nothing to do
101
+ process.exit(0);
102
+ }
103
+ let event;
104
+ try {
105
+ event = JSON.parse(eventJson);
106
+ }
107
+ catch {
108
+ process.exit(0);
109
+ }
110
+ if (!event.transcript_path) {
111
+ process.exit(0);
112
+ }
113
+ const plan = extractPlanFromTranscript(event.transcript_path);
114
+ if (!plan?.trim()) {
115
+ process.exit(0);
116
+ }
117
+ try {
118
+ await forestApi(config, "/plan", {
119
+ method: "POST",
120
+ body: {
121
+ plan,
122
+ sessionId: event.session_id,
123
+ cwd: event.cwd,
124
+ },
125
+ });
126
+ }
127
+ catch (err) {
128
+ // Log but don't block — this is async and best-effort
129
+ process.stderr.write(`Forest plan sync failed: ${err instanceof Error ? err.message : String(err)}\n`);
130
+ }
131
+ }
132
+ // --- Entrypoint ---
133
+ const subcommand = process.argv[2];
134
+ switch (subcommand) {
135
+ case "plan-exit":
136
+ handlePlanExit().catch(() => process.exit(0));
137
+ break;
138
+ default:
139
+ console.error(`Unknown hook: ${subcommand}`);
140
+ console.error("Available hooks: plan-exit");
141
+ process.exit(0); // don't block
142
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Shared HTTP client for Forest backend API.
3
+ * Used by both the MCP server and the hook script.
4
+ */
5
+ import type { ForestConfig } from "./config.js";
6
+ export declare function forestApi<T>(config: ForestConfig, path: string, options?: {
7
+ method?: string;
8
+ body?: unknown;
9
+ }): Promise<T>;
@@ -0,0 +1,20 @@
1
+ /**
2
+ * Shared HTTP client for Forest backend API.
3
+ * Used by both the MCP server and the hook script.
4
+ */
5
+ export async function forestApi(config, path, options = {}) {
6
+ const url = `${config.apiUrl}/mcp${path}`;
7
+ const response = await fetch(url, {
8
+ method: options.method ?? "GET",
9
+ headers: {
10
+ Authorization: `Bearer ${config.token}`,
11
+ "Content-Type": "application/json",
12
+ },
13
+ ...(options.body ? { body: JSON.stringify(options.body) } : {}),
14
+ });
15
+ if (!response.ok) {
16
+ const text = await response.text();
17
+ throw new Error(`Forest API error (${response.status}): ${text}`);
18
+ }
19
+ return response.json();
20
+ }
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Shared config — reads Forest credentials from:
3
+ * 1. Environment variables (FOREST_API_TOKEN, FOREST_API_URL)
4
+ * 2. Config file (~/.config/forest/config.json)
5
+ *
6
+ * The CLI `forest init` writes the config file. Both the MCP server
7
+ * and the hook script use this module to resolve credentials.
8
+ */
9
+ export interface ForestConfig {
10
+ token: string;
11
+ apiUrl: string;
12
+ }
13
+ export declare function readConfigFile(): Partial<ForestConfig>;
14
+ export declare function writeConfigFile(config: ForestConfig): void;
15
+ /**
16
+ * Resolve config: env vars take priority, then config file.
17
+ * Returns null if no token is found.
18
+ */
19
+ export declare function resolveConfig(): ForestConfig | null;
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Shared config — reads Forest credentials from:
3
+ * 1. Environment variables (FOREST_API_TOKEN, FOREST_API_URL)
4
+ * 2. Config file (~/.config/forest/config.json)
5
+ *
6
+ * The CLI `forest init` writes the config file. Both the MCP server
7
+ * and the hook script use this module to resolve credentials.
8
+ */
9
+ import { readFileSync, writeFileSync, mkdirSync } from "fs";
10
+ import { homedir } from "os";
11
+ import { join, dirname } from "path";
12
+ const DEFAULT_API_URL = "https://api.forestworks.ai";
13
+ function configPath() {
14
+ return join(homedir(), ".config", "forest", "config.json");
15
+ }
16
+ export function readConfigFile() {
17
+ try {
18
+ const raw = readFileSync(configPath(), "utf-8");
19
+ return JSON.parse(raw);
20
+ }
21
+ catch {
22
+ return {};
23
+ }
24
+ }
25
+ export function writeConfigFile(config) {
26
+ const path = configPath();
27
+ mkdirSync(dirname(path), { recursive: true });
28
+ writeFileSync(path, JSON.stringify(config, null, 2) + "\n", "utf-8");
29
+ }
30
+ /**
31
+ * Resolve config: env vars take priority, then config file.
32
+ * Returns null if no token is found.
33
+ */
34
+ export function resolveConfig() {
35
+ const file = readConfigFile();
36
+ const token = process.env.FOREST_API_TOKEN || file.token;
37
+ const apiUrl = process.env.FOREST_API_URL || file.apiUrl || DEFAULT_API_URL;
38
+ if (!token)
39
+ return null;
40
+ return { token, apiUrl };
41
+ }
package/build/mcp.d.ts ADDED
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forest MCP Server
4
+ *
5
+ * Bridges Claude Code sessions with your team via Forest.
6
+ * Allows syncing conversation context to Slack, viewing tasks,
7
+ * and updating task status — all from within Claude Code.
8
+ *
9
+ * Reads credentials from:
10
+ * 1. Env vars: FOREST_API_TOKEN, FOREST_API_URL
11
+ * 2. Config file: ~/.config/forest/config.json (written by `forest init`)
12
+ */
13
+ export {};
package/build/mcp.js ADDED
@@ -0,0 +1,191 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Forest MCP Server
4
+ *
5
+ * Bridges Claude Code sessions with your team via Forest.
6
+ * Allows syncing conversation context to Slack, viewing tasks,
7
+ * and updating task status — all from within Claude Code.
8
+ *
9
+ * Reads credentials from:
10
+ * 1. Env vars: FOREST_API_TOKEN, FOREST_API_URL
11
+ * 2. Config file: ~/.config/forest/config.json (written by `forest init`)
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 { resolveConfig } from "./lib/config.js";
17
+ import { forestApi } from "./lib/api.js";
18
+ const config = resolveConfig();
19
+ if (!config) {
20
+ console.error("Forest not configured. Run: npx @co-syn/forest init --token <token>");
21
+ console.error("Or set FOREST_API_TOKEN environment variable.");
22
+ process.exit(1);
23
+ }
24
+ // --- MCP Server ---
25
+ const server = new McpServer({
26
+ name: "forest",
27
+ version: "0.1.0",
28
+ });
29
+ // Tool 1: forest_sync — Share session context with the team via Slack
30
+ server.tool("forest_sync", "Share a summary of your current work session with your team via Slack. " +
31
+ "Use this when the user wants to share what they've been working on, " +
32
+ "communicate decisions, or ping someone about progress. " +
33
+ "You should summarize the relevant parts of the conversation as the summary parameter.", {
34
+ summary: z
35
+ .string()
36
+ .describe("A clear, concise summary of what was accomplished, decided, or discussed in this session. " +
37
+ "Include key decisions, code changes, blockers, or questions. " +
38
+ "Write it as if addressing the team — not as a chat transcript."),
39
+ recipients: z
40
+ .array(z.string())
41
+ .optional()
42
+ .describe("Slack usernames or display names to @mention in the message (e.g. ['john', 'sarah'])"),
43
+ channel: z
44
+ .string()
45
+ .optional()
46
+ .describe("Slack channel name to post to (e.g. 'engineering', 'general'). " +
47
+ "If omitted, posts to the team's default channel."),
48
+ taskId: z
49
+ .string()
50
+ .optional()
51
+ .describe("Forest task ID to link this sync to. If provided, the message will reference the task."),
52
+ }, async (args) => {
53
+ try {
54
+ const result = await forestApi(config, "/sync", {
55
+ method: "POST",
56
+ body: {
57
+ summary: args.summary,
58
+ recipients: args.recipients,
59
+ channel: args.channel,
60
+ taskId: args.taskId,
61
+ },
62
+ });
63
+ const parts = ["Synced to Slack"];
64
+ if (result.channel)
65
+ parts.push(`in #${result.channel}`);
66
+ if (args.recipients?.length)
67
+ parts.push(`(mentioned: ${args.recipients.join(", ")})`);
68
+ return {
69
+ content: [{ type: "text", text: parts.join(" ") }],
70
+ };
71
+ }
72
+ catch (error) {
73
+ return {
74
+ content: [
75
+ {
76
+ type: "text",
77
+ text: `Failed to sync: ${error instanceof Error ? error.message : String(error)}`,
78
+ },
79
+ ],
80
+ isError: true,
81
+ };
82
+ }
83
+ });
84
+ // Tool 2: forest_tasks — List active tasks for context
85
+ server.tool("forest_tasks", "List active tasks from Forest for your team. " +
86
+ "Use this to find task IDs when the user wants to link their work to a task, " +
87
+ "or to understand what the team is working on.", {
88
+ goalId: z
89
+ .string()
90
+ .optional()
91
+ .describe("Filter tasks by goal ID. If omitted, returns all active tasks."),
92
+ status: z
93
+ .string()
94
+ .optional()
95
+ .describe("Filter by status (unstarted, in_progress, blocked, at_risk, in_review). " +
96
+ "If omitted, returns all non-completed tasks."),
97
+ }, async (args) => {
98
+ try {
99
+ const params = new URLSearchParams();
100
+ if (args.goalId)
101
+ params.set("goalId", args.goalId);
102
+ if (args.status)
103
+ params.set("status", args.status);
104
+ const query = params.toString();
105
+ const result = await forestApi(config, `/tasks${query ? `?${query}` : ""}`);
106
+ if (result.tasks.length === 0) {
107
+ return {
108
+ content: [
109
+ { type: "text", text: "No active tasks found." },
110
+ ],
111
+ };
112
+ }
113
+ const formatted = result.tasks
114
+ .map((t) => `- [${t.status}] ${t.title} (ID: ${t.id})` +
115
+ (t.goalTitle ? `\n Goal: ${t.goalTitle}` : "") +
116
+ (t.assignee ? `\n Assignee: ${t.assignee}` : ""))
117
+ .join("\n\n");
118
+ return {
119
+ content: [
120
+ {
121
+ type: "text",
122
+ text: `Active tasks:\n\n${formatted}`,
123
+ },
124
+ ],
125
+ };
126
+ }
127
+ catch (error) {
128
+ return {
129
+ content: [
130
+ {
131
+ type: "text",
132
+ text: `Failed to fetch tasks: ${error instanceof Error ? error.message : String(error)}`,
133
+ },
134
+ ],
135
+ isError: true,
136
+ };
137
+ }
138
+ });
139
+ // Tool 3: forest_update — Update a task's status or add notes
140
+ server.tool("forest_update", "Update a Forest task with progress, status changes, or notes from your session. " +
141
+ "Use this when the user has completed work on a task or wants to record progress.", {
142
+ taskId: z.string().describe("The Forest task ID to update."),
143
+ status: z
144
+ .string()
145
+ .optional()
146
+ .describe("New status: unstarted, in_progress, blocked, at_risk, in_review, done, deferred, dropped"),
147
+ notes: z
148
+ .string()
149
+ .optional()
150
+ .describe("Progress notes to add to the task. Summarize what was done, " +
151
+ "decisions made, or blockers encountered."),
152
+ }, async (args) => {
153
+ try {
154
+ const result = await forestApi(config, `/tasks/${args.taskId}`, {
155
+ method: "POST",
156
+ body: {
157
+ status: args.status,
158
+ notes: args.notes,
159
+ },
160
+ });
161
+ const parts = [`Updated task: ${result.task.title}`];
162
+ if (args.status)
163
+ parts.push(`Status → ${result.task.status}`);
164
+ if (args.notes)
165
+ parts.push("Notes added");
166
+ return {
167
+ content: [{ type: "text", text: parts.join(". ") }],
168
+ };
169
+ }
170
+ catch (error) {
171
+ return {
172
+ content: [
173
+ {
174
+ type: "text",
175
+ text: `Failed to update task: ${error instanceof Error ? error.message : String(error)}`,
176
+ },
177
+ ],
178
+ isError: true,
179
+ };
180
+ }
181
+ });
182
+ // --- Start ---
183
+ async function main() {
184
+ const transport = new StdioServerTransport();
185
+ await server.connect(transport);
186
+ console.error("Forest MCP server running on stdio");
187
+ }
188
+ main().catch((err) => {
189
+ console.error("Forest MCP server failed to start:", err);
190
+ process.exit(1);
191
+ });
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@co-syn/forest",
3
+ "version": "0.1.0",
4
+ "description": "Forest CLI — sync Claude Code sessions, plans, and tasks with your team",
5
+ "keywords": [
6
+ "claude-code",
7
+ "forest",
8
+ "mcp",
9
+ "slack",
10
+ "team-sync"
11
+ ],
12
+ "homepage": "https://forestworks.ai",
13
+ "repository": {
14
+ "type": "git",
15
+ "url": "git+https://github.com/co-syn/forest.git",
16
+ "directory": "packages/forest-mcp"
17
+ },
18
+ "bugs": {
19
+ "url": "https://github.com/co-syn/forest/issues"
20
+ },
21
+ "type": "module",
22
+ "engines": {
23
+ "node": ">=20"
24
+ },
25
+ "bin": {
26
+ "forest": "build/cli.js",
27
+ "forest-mcp": "build/mcp.js",
28
+ "forest-hook": "build/hook.js"
29
+ },
30
+ "files": [
31
+ "build"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "scripts": {
37
+ "build": "tsc && chmod 755 build/cli.js build/mcp.js build/hook.js",
38
+ "dev": "tsx src/mcp.ts",
39
+ "prepublishOnly": "npm run build"
40
+ },
41
+ "dependencies": {
42
+ "@modelcontextprotocol/sdk": "^1.12.1",
43
+ "zod": "^3.24.0"
44
+ },
45
+ "devDependencies": {
46
+ "@types/node": "^22.0.0",
47
+ "tsx": "^4.21.0",
48
+ "typescript": "^5.7.0"
49
+ }
50
+ }