planflow-mcp 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js ADDED
@@ -0,0 +1,2950 @@
1
+ #!/usr/bin/env node
2
+
3
+ // src/server.ts
4
+ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
5
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6
+ import {
7
+ CallToolRequestSchema,
8
+ ListToolsRequestSchema
9
+ } from "@modelcontextprotocol/sdk/types.js";
10
+ import { APP_NAME, APP_VERSION } from "@planflow/shared";
11
+
12
+ // src/errors.ts
13
+ var PlanFlowError = class extends Error {
14
+ code;
15
+ details;
16
+ constructor(message, code, details) {
17
+ super(message);
18
+ this.name = "PlanFlowError";
19
+ this.code = code;
20
+ this.details = details;
21
+ if (Error.captureStackTrace) {
22
+ Error.captureStackTrace(this, this.constructor);
23
+ }
24
+ }
25
+ toJSON() {
26
+ return {
27
+ name: this.name,
28
+ message: this.message,
29
+ code: this.code,
30
+ details: this.details
31
+ };
32
+ }
33
+ };
34
+ var AuthError = class extends PlanFlowError {
35
+ constructor(message, details) {
36
+ super(message, "AUTH_ERROR", details);
37
+ this.name = "AuthError";
38
+ }
39
+ };
40
+ var ApiError = class extends PlanFlowError {
41
+ statusCode;
42
+ constructor(message, statusCode, details) {
43
+ super(message, "API_ERROR", details);
44
+ this.name = "ApiError";
45
+ this.statusCode = statusCode;
46
+ }
47
+ };
48
+ var ConfigError = class extends PlanFlowError {
49
+ constructor(message, details) {
50
+ super(message, "CONFIG_ERROR", details);
51
+ this.name = "ConfigError";
52
+ }
53
+ };
54
+ var ToolError = class extends PlanFlowError {
55
+ toolName;
56
+ constructor(message, toolName, details) {
57
+ super(message, "TOOL_ERROR", { ...details, toolName });
58
+ this.name = "ToolError";
59
+ this.toolName = toolName;
60
+ }
61
+ };
62
+
63
+ // src/logger.ts
64
+ var LOG_LEVELS = {
65
+ debug: 0,
66
+ info: 1,
67
+ warn: 2,
68
+ error: 3
69
+ };
70
+ var Logger = class {
71
+ minLevel = "info";
72
+ prefix = "[PlanFlow MCP]";
73
+ setLevel(level) {
74
+ this.minLevel = level;
75
+ }
76
+ shouldLog(level) {
77
+ return LOG_LEVELS[level] >= LOG_LEVELS[this.minLevel];
78
+ }
79
+ formatMessage(level, message, meta) {
80
+ const timestamp = (/* @__PURE__ */ new Date()).toISOString();
81
+ const levelStr = level.toUpperCase().padEnd(5);
82
+ let formatted = `${timestamp} ${levelStr} ${this.prefix} ${message}`;
83
+ if (meta && Object.keys(meta).length > 0) {
84
+ formatted += ` ${JSON.stringify(meta)}`;
85
+ }
86
+ return formatted;
87
+ }
88
+ debug(message, meta) {
89
+ if (this.shouldLog("debug")) {
90
+ console.error(this.formatMessage("debug", message, meta));
91
+ }
92
+ }
93
+ info(message, meta) {
94
+ if (this.shouldLog("info")) {
95
+ console.error(this.formatMessage("info", message, meta));
96
+ }
97
+ }
98
+ warn(message, meta) {
99
+ if (this.shouldLog("warn")) {
100
+ console.error(this.formatMessage("warn", message, meta));
101
+ }
102
+ }
103
+ error(message, meta) {
104
+ if (this.shouldLog("error")) {
105
+ console.error(this.formatMessage("error", message, meta));
106
+ }
107
+ }
108
+ };
109
+ var logger = new Logger();
110
+
111
+ // src/tools/login.ts
112
+ import { z as z2 } from "zod";
113
+
114
+ // src/config.ts
115
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
116
+ import { homedir } from "os";
117
+ import { join } from "path";
118
+ import { z } from "zod";
119
+ var ConfigSchema = z.object({
120
+ apiToken: z.string().optional(),
121
+ apiUrl: z.string().url().default("https://api.planflow.tools"),
122
+ userId: z.string().uuid().optional(),
123
+ userEmail: z.string().email().optional()
124
+ });
125
+ var DEFAULT_CONFIG = {
126
+ apiUrl: "https://api.planflow.tools"
127
+ };
128
+ function getConfigDir() {
129
+ const home = homedir();
130
+ return join(home, ".config", "planflow");
131
+ }
132
+ function getConfigPath() {
133
+ return join(getConfigDir(), "config.json");
134
+ }
135
+ function ensureConfigDir() {
136
+ const configDir = getConfigDir();
137
+ if (!existsSync(configDir)) {
138
+ mkdirSync(configDir, { recursive: true });
139
+ logger.debug("Created config directory", { path: configDir });
140
+ }
141
+ }
142
+ function loadConfig() {
143
+ const configPath = getConfigPath();
144
+ if (!existsSync(configPath)) {
145
+ logger.debug("No config file found, using defaults");
146
+ return DEFAULT_CONFIG;
147
+ }
148
+ try {
149
+ const content = readFileSync(configPath, "utf-8");
150
+ const parsed = JSON.parse(content);
151
+ const config = ConfigSchema.parse(parsed);
152
+ logger.debug("Loaded config from disk");
153
+ return config;
154
+ } catch (error) {
155
+ if (error instanceof z.ZodError) {
156
+ logger.warn("Invalid config file, using defaults", { errors: error.errors });
157
+ return DEFAULT_CONFIG;
158
+ }
159
+ throw new ConfigError("Failed to load configuration", { error: String(error) });
160
+ }
161
+ }
162
+ function saveConfig(config) {
163
+ ensureConfigDir();
164
+ const configPath = getConfigPath();
165
+ const existingConfig = loadConfig();
166
+ const newConfig = ConfigSchema.parse({ ...existingConfig, ...config });
167
+ try {
168
+ writeFileSync(configPath, JSON.stringify(newConfig, null, 2), "utf-8");
169
+ logger.debug("Saved config to disk");
170
+ return newConfig;
171
+ } catch (error) {
172
+ throw new ConfigError("Failed to save configuration", { error: String(error) });
173
+ }
174
+ }
175
+ function clearCredentials() {
176
+ const config = loadConfig();
177
+ saveConfig({
178
+ ...config,
179
+ apiToken: void 0,
180
+ userId: void 0,
181
+ userEmail: void 0
182
+ });
183
+ logger.info("Cleared stored credentials");
184
+ }
185
+ function isAuthenticated() {
186
+ const config = loadConfig();
187
+ return !!config.apiToken;
188
+ }
189
+ function getApiToken() {
190
+ const config = loadConfig();
191
+ if (!config.apiToken) {
192
+ throw new ConfigError("Not authenticated. Please run planflow_login first.");
193
+ }
194
+ return config.apiToken;
195
+ }
196
+ function getApiUrl() {
197
+ const config = loadConfig();
198
+ return config.apiUrl;
199
+ }
200
+
201
+ // src/api-client.ts
202
+ var DEFAULT_TIMEOUT = 3e4;
203
+ var MAX_RETRIES = 3;
204
+ var RETRY_DELAY = 1e3;
205
+ var ApiClient = class {
206
+ baseUrl;
207
+ token;
208
+ timeout;
209
+ constructor(options) {
210
+ this.baseUrl = getApiUrl();
211
+ this.token = null;
212
+ this.timeout = options?.timeout ?? DEFAULT_TIMEOUT;
213
+ }
214
+ // ============================================================
215
+ // Token Management
216
+ // ============================================================
217
+ /**
218
+ * Set authentication token directly
219
+ */
220
+ setToken(token) {
221
+ this.token = token;
222
+ logger.debug("API token set");
223
+ }
224
+ /**
225
+ * Clear the current token
226
+ */
227
+ clearToken() {
228
+ this.token = null;
229
+ logger.debug("API token cleared");
230
+ }
231
+ /**
232
+ * Load token from config file
233
+ * @returns true if token was loaded successfully
234
+ */
235
+ loadToken() {
236
+ try {
237
+ this.token = getApiToken();
238
+ logger.debug("API token loaded from config");
239
+ return true;
240
+ } catch {
241
+ this.token = null;
242
+ return false;
243
+ }
244
+ }
245
+ /**
246
+ * Check if client has a token set
247
+ */
248
+ hasToken() {
249
+ return this.token !== null;
250
+ }
251
+ /**
252
+ * Check if authenticated (has valid token in config)
253
+ */
254
+ isAuthenticated() {
255
+ return isAuthenticated();
256
+ }
257
+ /**
258
+ * Get the current API base URL
259
+ */
260
+ getBaseUrl() {
261
+ return this.baseUrl;
262
+ }
263
+ // ============================================================
264
+ // HTTP Request Methods
265
+ // ============================================================
266
+ /**
267
+ * Make an authenticated HTTP request with retry logic
268
+ */
269
+ async request(method, path, options) {
270
+ const { body, requireAuth = true, retries = MAX_RETRIES } = options ?? {};
271
+ if (requireAuth && !this.token) {
272
+ throw new AuthError("Not authenticated. Please run planflow_login first.");
273
+ }
274
+ const url = `${this.baseUrl}${path}`;
275
+ logger.debug("API request", { method, url });
276
+ let lastError = null;
277
+ for (let attempt = 1; attempt <= retries; attempt++) {
278
+ try {
279
+ const controller = new AbortController();
280
+ const timeoutId = setTimeout(() => controller.abort(), this.timeout);
281
+ const headers = {
282
+ "Content-Type": "application/json"
283
+ };
284
+ if (this.token) {
285
+ headers["Authorization"] = `Bearer ${this.token}`;
286
+ }
287
+ const response = await fetch(url, {
288
+ method,
289
+ headers,
290
+ body: body ? JSON.stringify(body) : void 0,
291
+ signal: controller.signal
292
+ });
293
+ clearTimeout(timeoutId);
294
+ const data = await response.json();
295
+ if (response.status === 401) {
296
+ throw new AuthError(
297
+ data.error ?? "Authentication failed. Your token may be invalid or expired.",
298
+ { statusCode: 401 }
299
+ );
300
+ }
301
+ if (response.status === 403) {
302
+ throw new AuthError(
303
+ data.error ?? "Access denied. You do not have permission to perform this action.",
304
+ { statusCode: 403 }
305
+ );
306
+ }
307
+ if (response.status === 404) {
308
+ throw new ApiError(data.error ?? "Resource not found", 404);
309
+ }
310
+ if (response.status === 400) {
311
+ throw new ApiError(data.error ?? "Invalid request", 400, {
312
+ details: data.details
313
+ });
314
+ }
315
+ if (response.status >= 500) {
316
+ throw new ApiError(
317
+ data.error ?? `Server error (${response.status})`,
318
+ response.status
319
+ );
320
+ }
321
+ if (!response.ok) {
322
+ throw new ApiError(
323
+ data.error ?? `Request failed with status ${response.status}`,
324
+ response.status
325
+ );
326
+ }
327
+ if (!data.success) {
328
+ throw new ApiError(data.error ?? "Request failed");
329
+ }
330
+ if (data.data === void 0) {
331
+ throw new ApiError("Empty response data");
332
+ }
333
+ logger.debug("API response success", { method, url, status: response.status });
334
+ return data.data;
335
+ } catch (error) {
336
+ lastError = error;
337
+ if (error instanceof AuthError) {
338
+ throw error;
339
+ }
340
+ if (error instanceof ApiError) {
341
+ const status = error.statusCode;
342
+ if (status && status >= 400 && status < 500 && status !== 408 && status !== 429) {
343
+ throw error;
344
+ }
345
+ }
346
+ if (error instanceof Error && error.name === "AbortError") {
347
+ lastError = new ApiError(`Request timeout after ${this.timeout}ms`, 408);
348
+ }
349
+ if (attempt < retries) {
350
+ const delay = RETRY_DELAY * Math.pow(2, attempt - 1);
351
+ logger.debug("Retrying request", { attempt, delay, error: String(error) });
352
+ await this.sleep(delay);
353
+ continue;
354
+ }
355
+ }
356
+ }
357
+ if (lastError instanceof ApiError || lastError instanceof AuthError) {
358
+ throw lastError;
359
+ }
360
+ throw new ApiError(
361
+ `Network error: ${lastError?.message ?? "Unknown error"}`,
362
+ void 0,
363
+ { originalError: String(lastError) }
364
+ );
365
+ }
366
+ /**
367
+ * Sleep for a given number of milliseconds
368
+ */
369
+ sleep(ms) {
370
+ return new Promise((resolve) => setTimeout(resolve, ms));
371
+ }
372
+ // ============================================================
373
+ // Auth Endpoints
374
+ // ============================================================
375
+ /**
376
+ * Get current authenticated user info
377
+ */
378
+ async getCurrentUser() {
379
+ return this.request("GET", "/auth/me");
380
+ }
381
+ /**
382
+ * Verify an API token (can be used without authentication)
383
+ */
384
+ async verifyToken(token) {
385
+ return this.request("POST", "/api-tokens/verify", {
386
+ body: { token },
387
+ requireAuth: false
388
+ });
389
+ }
390
+ // ============================================================
391
+ // Project Endpoints
392
+ // ============================================================
393
+ /**
394
+ * List all projects for the authenticated user
395
+ */
396
+ async listProjects() {
397
+ const response = await this.request("GET", "/projects");
398
+ return response.projects;
399
+ }
400
+ /**
401
+ * Get a single project by ID
402
+ */
403
+ async getProject(id) {
404
+ const response = await this.request("GET", `/projects/${id}`);
405
+ return response.project;
406
+ }
407
+ /**
408
+ * Create a new project
409
+ */
410
+ async createProject(data) {
411
+ const response = await this.request("POST", "/projects", {
412
+ body: data
413
+ });
414
+ return response.project;
415
+ }
416
+ /**
417
+ * Update an existing project
418
+ */
419
+ async updateProject(id, data) {
420
+ const response = await this.request("PUT", `/projects/${id}`, {
421
+ body: data
422
+ });
423
+ return response.project;
424
+ }
425
+ /**
426
+ * Delete a project
427
+ */
428
+ async deleteProject(id) {
429
+ await this.request("DELETE", `/projects/${id}`);
430
+ }
431
+ /**
432
+ * Get project plan content
433
+ */
434
+ async getProjectPlan(id) {
435
+ return this.request("GET", `/projects/${id}/plan`);
436
+ }
437
+ /**
438
+ * Update project plan content
439
+ */
440
+ async updateProjectPlan(id, plan) {
441
+ return this.request("PUT", `/projects/${id}/plan`, {
442
+ body: { plan }
443
+ });
444
+ }
445
+ // ============================================================
446
+ // Task Endpoints
447
+ // ============================================================
448
+ /**
449
+ * List all tasks for a project
450
+ */
451
+ async listTasks(projectId) {
452
+ return this.request("GET", `/projects/${projectId}/tasks`);
453
+ }
454
+ /**
455
+ * Update a single task by its task ID (e.g., "T1.1")
456
+ * This is a convenience wrapper around bulkUpdateTasks
457
+ */
458
+ async updateTask(projectId, taskUuid, updates) {
459
+ const response = await this.bulkUpdateTasks(projectId, [
460
+ { id: taskUuid, ...updates }
461
+ ]);
462
+ return response.tasks[0] ?? null;
463
+ }
464
+ /**
465
+ * Update task status by task ID (e.g., "T1.1")
466
+ * First fetches tasks to find the UUID, then updates
467
+ */
468
+ async updateTaskStatus(projectId, taskId, status) {
469
+ const { tasks } = await this.listTasks(projectId);
470
+ const task = tasks.find((t) => t.taskId === taskId);
471
+ if (!task) {
472
+ throw new ApiError(`Task ${taskId} not found in project`, 404);
473
+ }
474
+ return this.updateTask(projectId, task.id, { status });
475
+ }
476
+ /**
477
+ * Bulk update multiple tasks
478
+ */
479
+ async bulkUpdateTasks(projectId, tasks) {
480
+ return this.request("PUT", `/projects/${projectId}/tasks`, {
481
+ body: { tasks }
482
+ });
483
+ }
484
+ // ============================================================
485
+ // Notification Endpoints
486
+ // ============================================================
487
+ /**
488
+ * List notifications for the authenticated user
489
+ */
490
+ async listNotifications(options) {
491
+ const params = new URLSearchParams();
492
+ if (options?.projectId) params.append("projectId", options.projectId);
493
+ if (options?.unreadOnly) params.append("unreadOnly", "true");
494
+ if (options?.limit) params.append("limit", String(options.limit));
495
+ const query = params.toString();
496
+ const path = `/notifications${query ? "?" + query : ""}`;
497
+ return this.request("GET", path);
498
+ }
499
+ /**
500
+ * Mark a notification as read
501
+ */
502
+ async markNotificationRead(notificationId) {
503
+ return this.request("PUT", `/notifications/${notificationId}/read`);
504
+ }
505
+ /**
506
+ * Mark all notifications as read
507
+ */
508
+ async markAllNotificationsRead(projectId) {
509
+ const body = projectId ? { projectId } : void 0;
510
+ return this.request("PUT", "/notifications/read-all", { body });
511
+ }
512
+ // ============================================================
513
+ // Activity Endpoints
514
+ // ============================================================
515
+ /**
516
+ * Get activity log for a project
517
+ */
518
+ async getProjectActivity(projectId, options) {
519
+ const params = new URLSearchParams();
520
+ if (options?.action) params.append("action", options.action);
521
+ if (options?.entityType) params.append("entityType", options.entityType);
522
+ if (options?.taskId) params.append("taskId", options.taskId);
523
+ if (options?.limit) params.append("limit", String(options.limit));
524
+ if (options?.offset) params.append("offset", String(options.offset));
525
+ const query = params.toString();
526
+ const path = `/projects/${projectId}/activity${query ? "?" + query : ""}`;
527
+ return this.request("GET", path);
528
+ }
529
+ /**
530
+ * Get activity log for a specific task
531
+ */
532
+ async getTaskActivity(projectId, taskId, options) {
533
+ const params = new URLSearchParams();
534
+ if (options?.action) params.append("action", options.action);
535
+ if (options?.limit) params.append("limit", String(options.limit));
536
+ if (options?.offset) params.append("offset", String(options.offset));
537
+ const query = params.toString();
538
+ const path = `/projects/${projectId}/tasks/${taskId}/activity${query ? "?" + query : ""}`;
539
+ return this.request("GET", path);
540
+ }
541
+ // ============================================================
542
+ // Comment Endpoints
543
+ // ============================================================
544
+ /**
545
+ * List all comments for a task
546
+ */
547
+ async listComments(projectId, taskId) {
548
+ return this.request(
549
+ "GET",
550
+ `/projects/${projectId}/tasks/${taskId}/comments`
551
+ );
552
+ }
553
+ /**
554
+ * Get a single comment with replies
555
+ */
556
+ async getComment(projectId, taskId, commentId) {
557
+ return this.request(
558
+ "GET",
559
+ `/projects/${projectId}/tasks/${taskId}/comments/${commentId}`
560
+ );
561
+ }
562
+ /**
563
+ * Create a new comment on a task
564
+ */
565
+ async createComment(projectId, taskId, data) {
566
+ return this.request(
567
+ "POST",
568
+ `/projects/${projectId}/tasks/${taskId}/comments`,
569
+ { body: data }
570
+ );
571
+ }
572
+ /**
573
+ * Update a comment
574
+ */
575
+ async updateComment(projectId, taskId, commentId, data) {
576
+ return this.request(
577
+ "PUT",
578
+ `/projects/${projectId}/tasks/${taskId}/comments/${commentId}`,
579
+ { body: data }
580
+ );
581
+ }
582
+ /**
583
+ * Delete a comment
584
+ */
585
+ async deleteComment(projectId, taskId, commentId) {
586
+ await this.request(
587
+ "DELETE",
588
+ `/projects/${projectId}/tasks/${taskId}/comments/${commentId}`
589
+ );
590
+ }
591
+ // ============================================================
592
+ // Presence & Working On Endpoints (T6.1)
593
+ // ============================================================
594
+ /**
595
+ * Get presence list for a project
596
+ */
597
+ async getPresence(projectId) {
598
+ return this.request("GET", `/projects/${projectId}/presence`);
599
+ }
600
+ /**
601
+ * Start working on a task
602
+ */
603
+ async startWorkingOn(projectId, taskId) {
604
+ return this.request("POST", `/projects/${projectId}/tasks/${taskId}/work`, {
605
+ body: { action: "start" }
606
+ });
607
+ }
608
+ /**
609
+ * Stop working on current task
610
+ */
611
+ async stopWorkingOn(projectId) {
612
+ return this.request("POST", `/projects/${projectId}/tasks/_/work`, {
613
+ body: { action: "stop" }
614
+ });
615
+ }
616
+ // ============================================================
617
+ // Utility Methods
618
+ // ============================================================
619
+ /**
620
+ * Health check - verify API is reachable
621
+ */
622
+ async healthCheck() {
623
+ try {
624
+ const response = await fetch(`${this.baseUrl}/health`, {
625
+ method: "GET",
626
+ signal: AbortSignal.timeout(5e3)
627
+ });
628
+ return response.ok;
629
+ } catch {
630
+ return false;
631
+ }
632
+ }
633
+ /**
634
+ * Get API information
635
+ */
636
+ async getApiInfo() {
637
+ const response = await fetch(`${this.baseUrl}/`, {
638
+ method: "GET",
639
+ signal: AbortSignal.timeout(5e3)
640
+ });
641
+ if (!response.ok) {
642
+ throw new ApiError("Failed to get API info", response.status);
643
+ }
644
+ return response.json();
645
+ }
646
+ };
647
+ var apiClient = null;
648
+ function getApiClient() {
649
+ if (!apiClient) {
650
+ apiClient = new ApiClient();
651
+ apiClient.loadToken();
652
+ }
653
+ return apiClient;
654
+ }
655
+ function resetApiClient() {
656
+ apiClient = null;
657
+ }
658
+ function createApiClient(options) {
659
+ return new ApiClient(options);
660
+ }
661
+
662
+ // src/tools/types.ts
663
+ function createSuccessResult(text) {
664
+ return {
665
+ content: [{ type: "text", text }]
666
+ };
667
+ }
668
+ function createErrorResult(text) {
669
+ return {
670
+ content: [{ type: "text", text }],
671
+ isError: true
672
+ };
673
+ }
674
+ function formatTable(headers, rows, options) {
675
+ const padding = options?.padding ?? 2;
676
+ const widths = headers.map(
677
+ (h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
678
+ );
679
+ const headerRow = headers.map((h, i) => h.padEnd(widths[i])).join(" ".repeat(padding));
680
+ const separator = widths.map((w) => "-".repeat(w)).join(" ".repeat(padding));
681
+ const dataRows = rows.map(
682
+ (row) => row.map((cell, i) => (cell ?? "").padEnd(widths[i])).join(" ".repeat(padding))
683
+ );
684
+ return [headerRow, separator, ...dataRows].join("\n");
685
+ }
686
+ function formatKeyValue(pairs) {
687
+ const maxKeyLength = Math.max(...Object.keys(pairs).map((k) => k.length));
688
+ return Object.entries(pairs).map(([key, value]) => `${key.padEnd(maxKeyLength)}: ${String(value)}`).join("\n");
689
+ }
690
+
691
+ // src/tools/login.ts
692
+ var LoginInputSchema = z2.object({
693
+ token: z2.string().min(1, "API token is required")
694
+ });
695
+ var loginTool = {
696
+ name: "planflow_login",
697
+ description: `Authenticate with PlanFlow using an API token.
698
+
699
+ Get your API token from the PlanFlow dashboard at https://planflow.tools/settings/api-tokens
700
+
701
+ Usage:
702
+ planflow_login(token: "your-api-token")
703
+
704
+ After successful login, you can use other PlanFlow tools to manage your projects and tasks.`,
705
+ inputSchema: LoginInputSchema,
706
+ async execute(input) {
707
+ const { token } = input;
708
+ logger.info("Attempting to authenticate with PlanFlow");
709
+ if (isAuthenticated()) {
710
+ const config = loadConfig();
711
+ logger.debug("User already authenticated", { email: config.userEmail });
712
+ return createSuccessResult(
713
+ `\u26A0\uFE0F Already logged in as ${config.userEmail}
714
+
715
+ To switch accounts, run planflow_logout first, then login with the new token.`
716
+ );
717
+ }
718
+ try {
719
+ const client = createApiClient();
720
+ logger.debug("Verifying API token");
721
+ const verifyResponse = await client.verifyToken(token);
722
+ saveConfig({
723
+ apiToken: token,
724
+ userId: verifyResponse.user.id,
725
+ userEmail: verifyResponse.user.email
726
+ });
727
+ resetApiClient();
728
+ logger.info("Successfully authenticated", { email: verifyResponse.user.email });
729
+ const output = [
730
+ "\u2705 Successfully logged in to PlanFlow!\n",
731
+ formatKeyValue({
732
+ "User": verifyResponse.user.name,
733
+ "Email": verifyResponse.user.email,
734
+ "Token": verifyResponse.tokenName
735
+ }),
736
+ "\n\n\u{1F389} You can now use PlanFlow tools:",
737
+ " \u2022 planflow_projects - List your projects",
738
+ " \u2022 planflow_create - Create a new project",
739
+ " \u2022 planflow_sync - Sync project plans",
740
+ " \u2022 planflow_task_list - View project tasks",
741
+ " \u2022 planflow_whoami - Show current user info"
742
+ ].join("\n");
743
+ return createSuccessResult(output);
744
+ } catch (error) {
745
+ logger.error("Authentication failed", { error: String(error) });
746
+ if (error instanceof AuthError) {
747
+ return createErrorResult(
748
+ "\u274C Authentication failed: Invalid or expired API token.\n\nPlease check your token and try again.\nGet a new token at: https://planflow.tools/settings/api-tokens"
749
+ );
750
+ }
751
+ const message = error instanceof Error ? error.message : String(error);
752
+ return createErrorResult(
753
+ `\u274C Authentication failed: ${message}
754
+
755
+ Please check your internet connection and try again.`
756
+ );
757
+ }
758
+ }
759
+ };
760
+
761
+ // src/tools/logout.ts
762
+ import { z as z3 } from "zod";
763
+ var LogoutInputSchema = z3.object({});
764
+ var logoutTool = {
765
+ name: "planflow_logout",
766
+ description: `Log out from PlanFlow and clear stored credentials.
767
+
768
+ This will remove your API token from local storage. You will need to login again with planflow_login to use other PlanFlow tools.
769
+
770
+ Usage:
771
+ planflow_logout()
772
+
773
+ No parameters required.`,
774
+ inputSchema: LogoutInputSchema,
775
+ async execute(_input) {
776
+ logger.info("Attempting to logout from PlanFlow");
777
+ if (!isAuthenticated()) {
778
+ logger.debug("No active session found");
779
+ return createErrorResult(
780
+ "\u26A0\uFE0F Not currently logged in.\n\nUse planflow_login to authenticate first."
781
+ );
782
+ }
783
+ try {
784
+ const config = loadConfig();
785
+ const userEmail = config.userEmail ?? "unknown";
786
+ clearCredentials();
787
+ resetApiClient();
788
+ logger.info("Successfully logged out", { email: userEmail });
789
+ return createSuccessResult(
790
+ `\u2705 Successfully logged out from PlanFlow!
791
+
792
+ Goodbye, ${userEmail}!
793
+
794
+ \u{1F510} Your API token has been removed from local storage.
795
+
796
+ To login again, use:
797
+ planflow_login(token: "your-api-token")
798
+
799
+ Get your token at: https://planflow.tools/settings/api-tokens`
800
+ );
801
+ } catch (error) {
802
+ logger.error("Logout failed", { error: String(error) });
803
+ const message = error instanceof Error ? error.message : String(error);
804
+ return createErrorResult(
805
+ `\u274C Logout failed: ${message}
806
+
807
+ Please try again or manually delete the config file at:
808
+ ~/.config/planflow/config.json`
809
+ );
810
+ }
811
+ }
812
+ };
813
+
814
+ // src/tools/whoami.ts
815
+ import { z as z4 } from "zod";
816
+ var WhoamiInputSchema = z4.object({});
817
+ function formatDate(date) {
818
+ const d = typeof date === "string" ? new Date(date) : date;
819
+ return d.toLocaleDateString("en-US", {
820
+ year: "numeric",
821
+ month: "short",
822
+ day: "numeric",
823
+ hour: "2-digit",
824
+ minute: "2-digit"
825
+ });
826
+ }
827
+ var whoamiTool = {
828
+ name: "planflow_whoami",
829
+ description: `Show information about the currently authenticated PlanFlow user.
830
+
831
+ Displays your user profile including name, email, account creation date, and authentication method.
832
+
833
+ Usage:
834
+ planflow_whoami()
835
+
836
+ No parameters required. You must be logged in first with planflow_login.`,
837
+ inputSchema: WhoamiInputSchema,
838
+ async execute(_input) {
839
+ logger.info("Fetching current user information");
840
+ if (!isAuthenticated()) {
841
+ logger.debug("No active session found");
842
+ return createErrorResult(
843
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
844
+ );
845
+ }
846
+ try {
847
+ const client = getApiClient();
848
+ const response = await client.getCurrentUser();
849
+ const { user, authType } = response;
850
+ const config = loadConfig();
851
+ logger.info("Successfully retrieved user info", { email: user.email });
852
+ const output = [
853
+ "\u{1F464} Current User\n",
854
+ formatKeyValue({
855
+ "Name": user.name,
856
+ "Email": user.email,
857
+ "User ID": user.id,
858
+ "Auth Type": authType === "api-token" ? "API Token" : "JWT",
859
+ "Created": formatDate(user.createdAt),
860
+ "Updated": formatDate(user.updatedAt)
861
+ }),
862
+ "\n\n\u{1F4CA} Session Info",
863
+ formatKeyValue({
864
+ "API URL": config.apiUrl,
865
+ "Status": "\u2705 Connected"
866
+ }),
867
+ "\n\n\u{1F4A1} Available commands:",
868
+ " \u2022 planflow_projects - List your projects",
869
+ " \u2022 planflow_create - Create a new project",
870
+ " \u2022 planflow_sync - Sync project plans",
871
+ " \u2022 planflow_logout - Log out"
872
+ ].join("\n");
873
+ return createSuccessResult(output);
874
+ } catch (error) {
875
+ logger.error("Failed to fetch user info", { error: String(error) });
876
+ if (error instanceof AuthError) {
877
+ return createErrorResult(
878
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
879
+ );
880
+ }
881
+ if (error instanceof ApiError) {
882
+ return createErrorResult(
883
+ `\u274C API error: ${error.message}
884
+
885
+ Please check your internet connection and try again.`
886
+ );
887
+ }
888
+ const message = error instanceof Error ? error.message : String(error);
889
+ return createErrorResult(
890
+ `\u274C Failed to fetch user info: ${message}
891
+
892
+ Please try again or check your connection.`
893
+ );
894
+ }
895
+ }
896
+ };
897
+
898
+ // src/tools/projects.ts
899
+ import { z as z5 } from "zod";
900
+ var ProjectsInputSchema = z5.object({});
901
+ function formatDate2(date) {
902
+ const d = typeof date === "string" ? new Date(date) : date;
903
+ return d.toLocaleDateString("en-US", {
904
+ year: "numeric",
905
+ month: "short",
906
+ day: "numeric"
907
+ });
908
+ }
909
+ function truncate(str, maxLength) {
910
+ if (!str) return "-";
911
+ if (str.length <= maxLength) return str;
912
+ return str.slice(0, maxLength - 3) + "...";
913
+ }
914
+ var projectsTool = {
915
+ name: "planflow_projects",
916
+ description: `List all your PlanFlow projects.
917
+
918
+ Displays a table of all projects with their names, descriptions, and creation dates.
919
+
920
+ Usage:
921
+ planflow_projects()
922
+
923
+ No parameters required. You must be logged in first with planflow_login.
924
+
925
+ Returns:
926
+ - Project ID (use for other commands)
927
+ - Project name
928
+ - Description (truncated)
929
+ - Created date
930
+ - Updated date`,
931
+ inputSchema: ProjectsInputSchema,
932
+ async execute(_input) {
933
+ logger.info("Fetching projects list");
934
+ if (!isAuthenticated()) {
935
+ logger.debug("No active session found");
936
+ return createErrorResult(
937
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
938
+ );
939
+ }
940
+ try {
941
+ const client = getApiClient();
942
+ const projects = await client.listProjects();
943
+ logger.info("Successfully retrieved projects", { count: projects.length });
944
+ if (projects.length === 0) {
945
+ return createSuccessResult(
946
+ `\u{1F4C1} No projects found.
947
+
948
+ You don't have any projects yet.
949
+
950
+ \u{1F4A1} Create your first project:
951
+ planflow_create(name: "My Project", description: "Optional description")
952
+
953
+ Or create one at: https://planflow.tools/projects/new`
954
+ );
955
+ }
956
+ const headers = ["ID", "Name", "Description", "Created", "Updated"];
957
+ const rows = projects.map((project) => [
958
+ project.id.slice(0, 8) + "...",
959
+ // Show first 8 chars of UUID
960
+ truncate(project.name, 25),
961
+ truncate(project.description, 30),
962
+ formatDate2(project.createdAt),
963
+ formatDate2(project.updatedAt)
964
+ ]);
965
+ const output = [
966
+ `\u{1F4C1} Your Projects (${projects.length})
967
+ `,
968
+ formatTable(headers, rows),
969
+ "\n\n\u{1F4A1} Commands:",
970
+ ' \u2022 planflow_sync(projectId: "...") - Sync project plan',
971
+ ' \u2022 planflow_task_list(projectId: "...") - List project tasks',
972
+ ' \u2022 planflow_create(name: "...") - Create new project',
973
+ "\n\u{1F4CB} Full project IDs:",
974
+ ...projects.map((p) => ` \u2022 ${p.name}: ${p.id}`)
975
+ ].join("\n");
976
+ return createSuccessResult(output);
977
+ } catch (error) {
978
+ logger.error("Failed to fetch projects", { error: String(error) });
979
+ if (error instanceof AuthError) {
980
+ return createErrorResult(
981
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
982
+ );
983
+ }
984
+ if (error instanceof ApiError) {
985
+ return createErrorResult(
986
+ `\u274C API error: ${error.message}
987
+
988
+ Please check your internet connection and try again.`
989
+ );
990
+ }
991
+ const message = error instanceof Error ? error.message : String(error);
992
+ return createErrorResult(
993
+ `\u274C Failed to fetch projects: ${message}
994
+
995
+ Please try again or check your connection.`
996
+ );
997
+ }
998
+ }
999
+ };
1000
+
1001
+ // src/tools/create.ts
1002
+ import { z as z6 } from "zod";
1003
+ var CreateInputSchema = z6.object({
1004
+ name: z6.string().min(1, "Project name is required").max(255, "Project name must be at most 255 characters"),
1005
+ description: z6.string().max(1e3, "Description must be at most 1000 characters").optional()
1006
+ });
1007
+ function formatDate3(date) {
1008
+ const d = typeof date === "string" ? new Date(date) : date;
1009
+ return d.toLocaleDateString("en-US", {
1010
+ year: "numeric",
1011
+ month: "short",
1012
+ day: "numeric",
1013
+ hour: "2-digit",
1014
+ minute: "2-digit"
1015
+ });
1016
+ }
1017
+ var createTool = {
1018
+ name: "planflow_create",
1019
+ description: `Create a new PlanFlow project.
1020
+
1021
+ Creates a new project with the specified name and optional description.
1022
+
1023
+ Usage:
1024
+ planflow_create(name: "My Project")
1025
+ planflow_create(name: "My Project", description: "A description of my project")
1026
+
1027
+ Parameters:
1028
+ - name (required): Project name (1-255 characters)
1029
+ - description (optional): Project description (max 1000 characters)
1030
+
1031
+ You must be logged in first with planflow_login.
1032
+
1033
+ Returns:
1034
+ - Project ID (use for sync and task commands)
1035
+ - Project name
1036
+ - Description
1037
+ - Created timestamp`,
1038
+ inputSchema: CreateInputSchema,
1039
+ async execute(input) {
1040
+ logger.info("Creating new project", { name: input.name });
1041
+ if (!isAuthenticated()) {
1042
+ logger.debug("No active session found");
1043
+ return createErrorResult(
1044
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
1045
+ );
1046
+ }
1047
+ try {
1048
+ const client = getApiClient();
1049
+ const project = await client.createProject({
1050
+ name: input.name,
1051
+ description: input.description
1052
+ });
1053
+ logger.info("Successfully created project", { projectId: project.id });
1054
+ const output = [
1055
+ "\u2705 Project created successfully!\n",
1056
+ formatKeyValue({
1057
+ "Project ID": project.id,
1058
+ "Name": project.name,
1059
+ "Description": project.description || "(none)",
1060
+ "Created": formatDate3(project.createdAt)
1061
+ }),
1062
+ "\n\n\u{1F4A1} Next steps:",
1063
+ ' \u2022 planflow_sync(projectId: "' + project.id + '", direction: "push") - Upload your PROJECT_PLAN.md',
1064
+ ' \u2022 planflow_task_list(projectId: "' + project.id + '") - View project tasks',
1065
+ " \u2022 planflow_projects() - List all projects",
1066
+ "\n\u{1F4CB} Save this project ID for future commands:",
1067
+ ` ${project.id}`
1068
+ ].join("\n");
1069
+ return createSuccessResult(output);
1070
+ } catch (error) {
1071
+ logger.error("Failed to create project", { error: String(error) });
1072
+ if (error instanceof AuthError) {
1073
+ return createErrorResult(
1074
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
1075
+ );
1076
+ }
1077
+ if (error instanceof ApiError) {
1078
+ if (error.statusCode === 400) {
1079
+ return createErrorResult(
1080
+ `\u274C Invalid project data: ${error.message}
1081
+
1082
+ Please check:
1083
+ \u2022 Name is between 1-255 characters
1084
+ \u2022 Description is at most 1000 characters`
1085
+ );
1086
+ }
1087
+ return createErrorResult(
1088
+ `\u274C API error: ${error.message}
1089
+
1090
+ Please check your internet connection and try again.`
1091
+ );
1092
+ }
1093
+ const message = error instanceof Error ? error.message : String(error);
1094
+ return createErrorResult(
1095
+ `\u274C Failed to create project: ${message}
1096
+
1097
+ Please try again or check your connection.`
1098
+ );
1099
+ }
1100
+ }
1101
+ };
1102
+
1103
+ // src/tools/sync.ts
1104
+ import { z as z7 } from "zod";
1105
+ var SyncInputSchema = z7.object({
1106
+ projectId: z7.string().uuid("Invalid project ID format"),
1107
+ direction: z7.enum(["push", "pull"]),
1108
+ content: z7.string().optional()
1109
+ // Required for push, ignored for pull
1110
+ });
1111
+ function formatDate4(date) {
1112
+ const d = typeof date === "string" ? new Date(date) : date;
1113
+ return d.toLocaleDateString("en-US", {
1114
+ year: "numeric",
1115
+ month: "short",
1116
+ day: "numeric",
1117
+ hour: "2-digit",
1118
+ minute: "2-digit"
1119
+ });
1120
+ }
1121
+ function countLines(content) {
1122
+ if (!content) return 0;
1123
+ return content.split("\n").length;
1124
+ }
1125
+ function formatBytes(bytes) {
1126
+ if (bytes < 1024) return `${bytes} bytes`;
1127
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
1128
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1129
+ }
1130
+ var syncTool = {
1131
+ name: "planflow_sync",
1132
+ description: `Sync PROJECT_PLAN.md with PlanFlow cloud.
1133
+
1134
+ Bidirectional synchronization between local plan files and the cloud.
1135
+
1136
+ Usage:
1137
+ planflow_sync(projectId: "uuid", direction: "push", content: "# Plan...") - Upload local plan
1138
+ planflow_sync(projectId: "uuid", direction: "pull") - Download cloud plan
1139
+
1140
+ Parameters:
1141
+ - projectId (required): Project UUID from planflow_create or planflow_projects
1142
+ - direction (required): "push" to upload, "pull" to download
1143
+ - content (required for push): The markdown content to upload
1144
+
1145
+ You must be logged in first with planflow_login.
1146
+
1147
+ Returns:
1148
+ - Push: Confirmation with size and timestamp
1149
+ - Pull: Full plan content (markdown) to save locally`,
1150
+ inputSchema: SyncInputSchema,
1151
+ async execute(input) {
1152
+ logger.info("Syncing project plan", {
1153
+ projectId: input.projectId,
1154
+ direction: input.direction
1155
+ });
1156
+ if (!isAuthenticated()) {
1157
+ logger.debug("No active session found");
1158
+ return createErrorResult(
1159
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
1160
+ );
1161
+ }
1162
+ if (input.direction === "push" && !input.content) {
1163
+ logger.debug("Push requested without content");
1164
+ return createErrorResult(
1165
+ `\u274C Content is required for push operation.
1166
+
1167
+ Please provide the plan content:
1168
+ planflow_sync(
1169
+ projectId: "${input.projectId}",
1170
+ direction: "push",
1171
+ content: "# Your plan content here..."
1172
+ )
1173
+
1174
+ \u{1F4A1} Tip: Read your PROJECT_PLAN.md file and pass its content.`
1175
+ );
1176
+ }
1177
+ try {
1178
+ const client = getApiClient();
1179
+ if (input.direction === "push") {
1180
+ return await executePush(client, input.projectId, input.content);
1181
+ } else {
1182
+ return await executePull(client, input.projectId);
1183
+ }
1184
+ } catch (error) {
1185
+ logger.error("Failed to sync project plan", { error: String(error) });
1186
+ if (error instanceof AuthError) {
1187
+ return createErrorResult(
1188
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
1189
+ );
1190
+ }
1191
+ if (error instanceof ApiError) {
1192
+ if (error.statusCode === 404) {
1193
+ return createErrorResult(
1194
+ "\u274C Project not found.\n\nPlease check the project ID and try again.\nRun planflow_projects() to list your available projects."
1195
+ );
1196
+ }
1197
+ return createErrorResult(
1198
+ `\u274C API error: ${error.message}
1199
+
1200
+ Please check your internet connection and try again.`
1201
+ );
1202
+ }
1203
+ const message = error instanceof Error ? error.message : String(error);
1204
+ return createErrorResult(
1205
+ `\u274C Failed to sync plan: ${message}
1206
+
1207
+ Please try again or check your connection.`
1208
+ );
1209
+ }
1210
+ }
1211
+ };
1212
+ async function executePush(client, projectId, content) {
1213
+ logger.info("Pushing plan to cloud", { projectId, contentLength: content.length });
1214
+ const response = await client.updateProjectPlan(projectId, content);
1215
+ const bytes = new TextEncoder().encode(content).length;
1216
+ const lines = countLines(content);
1217
+ const output = [
1218
+ "\u2705 Plan synced to cloud!\n",
1219
+ formatKeyValue({
1220
+ "Project": response.projectName,
1221
+ "Direction": "push",
1222
+ "Size": `${formatBytes(bytes)} (${lines} lines)`,
1223
+ "Updated": formatDate4(response.updatedAt)
1224
+ }),
1225
+ "\n\n\u{1F4A1} Tip: Your local changes are now saved to the cloud."
1226
+ ].join("\n");
1227
+ logger.info("Successfully pushed plan", { projectId });
1228
+ return createSuccessResult(output);
1229
+ }
1230
+ async function executePull(client, projectId) {
1231
+ logger.info("Pulling plan from cloud", { projectId });
1232
+ const response = await client.getProjectPlan(projectId);
1233
+ if (!response.plan) {
1234
+ const output2 = [
1235
+ "\u26A0\uFE0F No plan exists for this project yet.\n",
1236
+ formatKeyValue({
1237
+ "Project": response.projectName,
1238
+ "Project ID": response.projectId
1239
+ }),
1240
+ "\n\n\u{1F4A1} Tip: Create a PROJECT_PLAN.md locally and use 'push' to upload it."
1241
+ ].join("\n");
1242
+ return createSuccessResult(output2);
1243
+ }
1244
+ const bytes = new TextEncoder().encode(response.plan).length;
1245
+ const lines = countLines(response.plan);
1246
+ const output = [
1247
+ "\u2705 Plan retrieved from cloud!\n",
1248
+ formatKeyValue({
1249
+ "Project": response.projectName,
1250
+ "Direction": "pull",
1251
+ "Size": `${formatBytes(bytes)} (${lines} lines)`,
1252
+ "Updated": formatDate4(response.updatedAt)
1253
+ }),
1254
+ "\n\n---",
1255
+ response.plan,
1256
+ "---",
1257
+ "\n\u{1F4A1} Tip: Save this content to PROJECT_PLAN.md in your project."
1258
+ ].join("\n");
1259
+ logger.info("Successfully pulled plan", { projectId, contentLength: response.plan.length });
1260
+ return createSuccessResult(output);
1261
+ }
1262
+
1263
+ // src/tools/task-list.ts
1264
+ import { z as z8 } from "zod";
1265
+ var TaskListInputSchema = z8.object({
1266
+ projectId: z8.string().uuid("Project ID must be a valid UUID"),
1267
+ status: z8.enum(["TODO", "IN_PROGRESS", "DONE", "BLOCKED"]).optional().describe("Filter tasks by status")
1268
+ });
1269
+ function truncate2(str, maxLength) {
1270
+ if (!str) return "-";
1271
+ if (str.length <= maxLength) return str;
1272
+ return str.slice(0, maxLength - 3) + "...";
1273
+ }
1274
+ function getStatusEmoji(status) {
1275
+ switch (status) {
1276
+ case "TODO":
1277
+ return "\u{1F4CB}";
1278
+ case "IN_PROGRESS":
1279
+ return "\u{1F504}";
1280
+ case "DONE":
1281
+ return "\u2705";
1282
+ case "BLOCKED":
1283
+ return "\u{1F6AB}";
1284
+ default:
1285
+ return "\u2753";
1286
+ }
1287
+ }
1288
+ function getComplexityIndicator(complexity) {
1289
+ switch (complexity) {
1290
+ case "Low":
1291
+ return "\u{1F7E2}";
1292
+ case "Medium":
1293
+ return "\u{1F7E1}";
1294
+ case "High":
1295
+ return "\u{1F534}";
1296
+ default:
1297
+ return "\u26AA";
1298
+ }
1299
+ }
1300
+ var taskListTool = {
1301
+ name: "planflow_task_list",
1302
+ description: `List all tasks for a PlanFlow project.
1303
+
1304
+ Displays a table of tasks with their status, complexity, and dependencies.
1305
+
1306
+ Usage:
1307
+ planflow_task_list(projectId: "uuid")
1308
+ planflow_task_list(projectId: "uuid", status: "TODO")
1309
+
1310
+ Parameters:
1311
+ - projectId (required): The project UUID (get from planflow_projects)
1312
+ - status (optional): Filter by status - TODO, IN_PROGRESS, DONE, or BLOCKED
1313
+
1314
+ Returns:
1315
+ - Task ID (e.g., T1.1)
1316
+ - Task name
1317
+ - Status with emoji
1318
+ - Complexity indicator
1319
+ - Estimated hours
1320
+ - Dependencies
1321
+
1322
+ You must be logged in first with planflow_login.`,
1323
+ inputSchema: TaskListInputSchema,
1324
+ async execute(input) {
1325
+ logger.info("Fetching tasks list", { projectId: input.projectId, status: input.status });
1326
+ if (!isAuthenticated()) {
1327
+ logger.debug("No active session found");
1328
+ return createErrorResult(
1329
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
1330
+ );
1331
+ }
1332
+ try {
1333
+ const client = getApiClient();
1334
+ const response = await client.listTasks(input.projectId);
1335
+ logger.info("Successfully retrieved tasks", {
1336
+ projectId: input.projectId,
1337
+ count: response.tasks.length
1338
+ });
1339
+ let tasks = response.tasks;
1340
+ if (input.status) {
1341
+ tasks = tasks.filter((t) => t.status === input.status);
1342
+ logger.debug("Filtered tasks by status", {
1343
+ status: input.status,
1344
+ filteredCount: tasks.length
1345
+ });
1346
+ }
1347
+ if (tasks.length === 0) {
1348
+ const filterMessage = input.status ? ` with status "${input.status}"` : "";
1349
+ return createSuccessResult(
1350
+ `\u{1F4CB} No tasks found${filterMessage}.
1351
+
1352
+ Project: ${response.projectName}
1353
+
1354
+ ` + (input.status ? `\u{1F4A1} Try removing the status filter to see all tasks:
1355
+ planflow_task_list(projectId: "${input.projectId}")` : `\u{1F4A1} Tasks are created when you sync your PROJECT_PLAN.md:
1356
+ planflow_sync(projectId: "${input.projectId}", direction: "push")`)
1357
+ );
1358
+ }
1359
+ tasks.sort((a, b) => {
1360
+ const parseTaskId = (id) => {
1361
+ const match = id.match(/T(\d+)\.(\d+)/);
1362
+ if (!match) return [0, 0];
1363
+ return [parseInt(match[1], 10), parseInt(match[2], 10)];
1364
+ };
1365
+ const [aMajor, aMinor] = parseTaskId(a.taskId);
1366
+ const [bMajor, bMinor] = parseTaskId(b.taskId);
1367
+ if (aMajor !== bMajor) return aMajor - bMajor;
1368
+ return aMinor - bMinor;
1369
+ });
1370
+ const stats = {
1371
+ total: response.tasks.length,
1372
+ todo: response.tasks.filter((t) => t.status === "TODO").length,
1373
+ inProgress: response.tasks.filter((t) => t.status === "IN_PROGRESS").length,
1374
+ done: response.tasks.filter((t) => t.status === "DONE").length,
1375
+ blocked: response.tasks.filter((t) => t.status === "BLOCKED").length
1376
+ };
1377
+ const progressPercent = Math.round(stats.done / stats.total * 100);
1378
+ const headers = ["ID", "Name", "Status", "Complexity", "Est.", "Dependencies"];
1379
+ const rows = tasks.map((task) => [
1380
+ task.taskId,
1381
+ truncate2(task.name, 30),
1382
+ `${getStatusEmoji(task.status)} ${task.status}`,
1383
+ `${getComplexityIndicator(task.complexity)} ${task.complexity}`,
1384
+ task.estimatedHours ? `${task.estimatedHours}h` : "-",
1385
+ task.dependencies.length > 0 ? task.dependencies.join(", ") : "-"
1386
+ ]);
1387
+ const progressBarLength = 10;
1388
+ const filledBlocks = Math.floor(progressPercent / 10);
1389
+ const progressBar = "\u{1F7E9}".repeat(filledBlocks) + "\u2B1C".repeat(progressBarLength - filledBlocks);
1390
+ const filterLabel = input.status ? ` (filtered: ${input.status})` : "";
1391
+ const output = [
1392
+ `\u{1F4CB} Tasks for "${response.projectName}"${filterLabel}
1393
+ `,
1394
+ `Progress: ${progressBar} ${progressPercent}%`,
1395
+ `Total: ${stats.total} | \u2705 ${stats.done} | \u{1F504} ${stats.inProgress} | \u{1F4CB} ${stats.todo} | \u{1F6AB} ${stats.blocked}
1396
+ `,
1397
+ formatTable(headers, rows),
1398
+ "\n\n\u{1F4A1} Commands:",
1399
+ ` \u2022 planflow_task_update(projectId: "${input.projectId}", taskId: "T1.1", status: "DONE")`,
1400
+ ` \u2022 planflow_task_list(projectId: "${input.projectId}", status: "TODO")`,
1401
+ ` \u2022 planflow_sync(projectId: "${input.projectId}", direction: "pull")`
1402
+ ].join("\n");
1403
+ return createSuccessResult(output);
1404
+ } catch (error) {
1405
+ logger.error("Failed to fetch tasks", { error: String(error), projectId: input.projectId });
1406
+ if (error instanceof AuthError) {
1407
+ return createErrorResult(
1408
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
1409
+ );
1410
+ }
1411
+ if (error instanceof ApiError) {
1412
+ if (error.statusCode === 404) {
1413
+ return createErrorResult(
1414
+ `\u274C Project not found: ${input.projectId}
1415
+
1416
+ Please check the project ID and try again.
1417
+ Use planflow_projects() to list your available projects.`
1418
+ );
1419
+ }
1420
+ return createErrorResult(
1421
+ `\u274C API error: ${error.message}
1422
+
1423
+ Please check your internet connection and try again.`
1424
+ );
1425
+ }
1426
+ const message = error instanceof Error ? error.message : String(error);
1427
+ return createErrorResult(
1428
+ `\u274C Failed to fetch tasks: ${message}
1429
+
1430
+ Please try again or check your connection.`
1431
+ );
1432
+ }
1433
+ }
1434
+ };
1435
+
1436
+ // src/tools/task-update.ts
1437
+ import { z as z9 } from "zod";
1438
+ var TaskUpdateInputSchema = z9.object({
1439
+ projectId: z9.string().uuid("Project ID must be a valid UUID"),
1440
+ taskId: z9.string().describe('Task ID (e.g., "T1.1", "T2.3")'),
1441
+ status: z9.enum(["TODO", "IN_PROGRESS", "DONE", "BLOCKED"]).describe("New status for the task")
1442
+ });
1443
+ function getStatusEmoji2(status) {
1444
+ switch (status) {
1445
+ case "TODO":
1446
+ return "\u{1F4CB}";
1447
+ case "IN_PROGRESS":
1448
+ return "\u{1F504}";
1449
+ case "DONE":
1450
+ return "\u2705";
1451
+ case "BLOCKED":
1452
+ return "\u{1F6AB}";
1453
+ default:
1454
+ return "\u2753";
1455
+ }
1456
+ }
1457
+ function getComplexityIndicator2(complexity) {
1458
+ switch (complexity) {
1459
+ case "Low":
1460
+ return "\u{1F7E2}";
1461
+ case "Medium":
1462
+ return "\u{1F7E1}";
1463
+ case "High":
1464
+ return "\u{1F534}";
1465
+ default:
1466
+ return "\u26AA";
1467
+ }
1468
+ }
1469
+ var taskUpdateTool = {
1470
+ name: "planflow_task_update",
1471
+ description: `Update task status in a PlanFlow project.
1472
+
1473
+ Changes the status of a specific task (e.g., mark as done, in progress, or blocked).
1474
+
1475
+ Usage:
1476
+ planflow_task_update(projectId: "uuid", taskId: "T1.1", status: "IN_PROGRESS")
1477
+ planflow_task_update(projectId: "uuid", taskId: "T2.3", status: "DONE")
1478
+
1479
+ Parameters:
1480
+ - projectId (required): The project UUID (get from planflow_projects)
1481
+ - taskId (required): The task ID (e.g., "T1.1", "T2.3")
1482
+ - status (required): New status - TODO, IN_PROGRESS, DONE, or BLOCKED
1483
+
1484
+ Status meanings:
1485
+ - TODO: Task not yet started
1486
+ - IN_PROGRESS: Currently working on this task
1487
+ - DONE: Task completed
1488
+ - BLOCKED: Task cannot proceed (document blocker reason)
1489
+
1490
+ You must be logged in first with planflow_login.`,
1491
+ inputSchema: TaskUpdateInputSchema,
1492
+ async execute(input) {
1493
+ logger.info("Updating task", {
1494
+ projectId: input.projectId,
1495
+ taskId: input.taskId,
1496
+ status: input.status
1497
+ });
1498
+ if (!isAuthenticated()) {
1499
+ logger.debug("No active session found");
1500
+ return createErrorResult(
1501
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
1502
+ );
1503
+ }
1504
+ try {
1505
+ const client = getApiClient();
1506
+ const updatedTask = await client.updateTaskStatus(
1507
+ input.projectId,
1508
+ input.taskId,
1509
+ input.status
1510
+ );
1511
+ if (!updatedTask) {
1512
+ return createErrorResult(
1513
+ `\u274C Failed to update task ${input.taskId}.
1514
+
1515
+ The task may not exist or the update was rejected.
1516
+ Use planflow_task_list() to see available tasks.`
1517
+ );
1518
+ }
1519
+ logger.info("Successfully updated task", {
1520
+ taskId: input.taskId,
1521
+ newStatus: input.status
1522
+ });
1523
+ try {
1524
+ if (input.status === "IN_PROGRESS") {
1525
+ await client.startWorkingOn(input.projectId, input.taskId);
1526
+ logger.debug("Auto-started working on task", { taskId: input.taskId });
1527
+ } else if (input.status === "DONE") {
1528
+ await client.stopWorkingOn(input.projectId);
1529
+ logger.debug("Auto-stopped working on task", { taskId: input.taskId });
1530
+ }
1531
+ } catch (workingOnError) {
1532
+ logger.debug("Failed to update working on status (non-fatal)", {
1533
+ error: String(workingOnError)
1534
+ });
1535
+ }
1536
+ const statusEmoji = getStatusEmoji2(updatedTask.status);
1537
+ const complexityIndicator = getComplexityIndicator2(updatedTask.complexity);
1538
+ const taskDetails = formatKeyValue({
1539
+ "Task ID": updatedTask.taskId,
1540
+ "Name": updatedTask.name,
1541
+ "Status": `${statusEmoji} ${updatedTask.status}`,
1542
+ "Complexity": `${complexityIndicator} ${updatedTask.complexity}`,
1543
+ "Estimated": updatedTask.estimatedHours ? `${updatedTask.estimatedHours}h` : "-",
1544
+ "Dependencies": updatedTask.dependencies.length > 0 ? updatedTask.dependencies.join(", ") : "None"
1545
+ });
1546
+ let nextSteps;
1547
+ switch (input.status) {
1548
+ case "IN_PROGRESS":
1549
+ nextSteps = [
1550
+ "\n\u{1F4A1} Next steps:",
1551
+ ` \u2022 When finished: planflow_task_update(projectId: "${input.projectId}", taskId: "${input.taskId}", status: "DONE")`,
1552
+ ` \u2022 If blocked: planflow_task_update(projectId: "${input.projectId}", taskId: "${input.taskId}", status: "BLOCKED")`,
1553
+ ` \u2022 Sync changes: planflow_sync(projectId: "${input.projectId}", direction: "pull")`
1554
+ ].join("\n");
1555
+ break;
1556
+ case "DONE":
1557
+ nextSteps = [
1558
+ "\n\u{1F389} Great work!",
1559
+ "\n\u{1F4A1} Next steps:",
1560
+ ` \u2022 Find next task: planflow_task_list(projectId: "${input.projectId}", status: "TODO")`,
1561
+ ` \u2022 Sync changes: planflow_sync(projectId: "${input.projectId}", direction: "pull")`
1562
+ ].join("\n");
1563
+ break;
1564
+ case "BLOCKED":
1565
+ nextSteps = [
1566
+ "\n\u26A0\uFE0F Task blocked - document the blocker:",
1567
+ " \u2022 What is blocking this task?",
1568
+ " \u2022 What needs to happen to unblock it?",
1569
+ " \u2022 Who can help resolve this?",
1570
+ "\n\u{1F4A1} When unblocked:",
1571
+ ` \u2022 planflow_task_update(projectId: "${input.projectId}", taskId: "${input.taskId}", status: "IN_PROGRESS")`
1572
+ ].join("\n");
1573
+ break;
1574
+ default:
1575
+ nextSteps = [
1576
+ "\n\u{1F4A1} Commands:",
1577
+ ` \u2022 Start task: planflow_task_update(projectId: "${input.projectId}", taskId: "${input.taskId}", status: "IN_PROGRESS")`,
1578
+ ` \u2022 View all tasks: planflow_task_list(projectId: "${input.projectId}")`
1579
+ ].join("\n");
1580
+ }
1581
+ const output = [
1582
+ `${statusEmoji} Task ${input.taskId} updated to ${input.status}!
1583
+ `,
1584
+ taskDetails,
1585
+ nextSteps
1586
+ ].join("\n");
1587
+ return createSuccessResult(output);
1588
+ } catch (error) {
1589
+ logger.error("Failed to update task", {
1590
+ error: String(error),
1591
+ projectId: input.projectId,
1592
+ taskId: input.taskId
1593
+ });
1594
+ if (error instanceof AuthError) {
1595
+ return createErrorResult(
1596
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
1597
+ );
1598
+ }
1599
+ if (error instanceof ApiError) {
1600
+ if (error.statusCode === 404) {
1601
+ return createErrorResult(
1602
+ `\u274C Task not found: ${input.taskId}
1603
+
1604
+ Please check the task ID and try again.
1605
+ Use planflow_task_list(projectId: "${input.projectId}") to see available tasks.`
1606
+ );
1607
+ }
1608
+ return createErrorResult(
1609
+ `\u274C API error: ${error.message}
1610
+
1611
+ Please check your internet connection and try again.`
1612
+ );
1613
+ }
1614
+ const message = error instanceof Error ? error.message : String(error);
1615
+ return createErrorResult(
1616
+ `\u274C Failed to update task: ${message}
1617
+
1618
+ Please try again or check your connection.`
1619
+ );
1620
+ }
1621
+ }
1622
+ };
1623
+
1624
+ // src/tools/task-next.ts
1625
+ import { z as z10 } from "zod";
1626
+ var TaskNextInputSchema = z10.object({
1627
+ projectId: z10.string().uuid("Project ID must be a valid UUID")
1628
+ });
1629
+ function getComplexityIndicator3(complexity) {
1630
+ switch (complexity) {
1631
+ case "Low":
1632
+ return "\u{1F7E2}";
1633
+ case "Medium":
1634
+ return "\u{1F7E1}";
1635
+ case "High":
1636
+ return "\u{1F534}";
1637
+ default:
1638
+ return "\u26AA";
1639
+ }
1640
+ }
1641
+ function parsePhase(taskId) {
1642
+ const match = taskId.match(/T(\d+)\./);
1643
+ return match ? parseInt(match[1], 10) : 0;
1644
+ }
1645
+ function parseTaskOrder(taskId) {
1646
+ const match = taskId.match(/T\d+\.(\d+)/);
1647
+ return match ? parseInt(match[1], 10) : 0;
1648
+ }
1649
+ function areDependenciesSatisfied(taskDependencies, completedTaskIds) {
1650
+ return taskDependencies.every((dep) => completedTaskIds.has(dep));
1651
+ }
1652
+ function countDependentTasks(taskId, allTasks) {
1653
+ return allTasks.filter((t) => t.dependencies.includes(taskId)).length;
1654
+ }
1655
+ function findCurrentPhase(tasks) {
1656
+ const phaseMap = /* @__PURE__ */ new Map();
1657
+ for (const task of tasks) {
1658
+ const phase = parsePhase(task.taskId);
1659
+ if (!phaseMap.has(phase)) {
1660
+ phaseMap.set(phase, { total: 0, done: 0 });
1661
+ }
1662
+ const stats = phaseMap.get(phase);
1663
+ stats.total++;
1664
+ if (task.status === "DONE") {
1665
+ stats.done++;
1666
+ }
1667
+ }
1668
+ const phases = Array.from(phaseMap.keys()).sort((a, b) => a - b);
1669
+ for (const phase of phases) {
1670
+ const stats = phaseMap.get(phase);
1671
+ if (stats.done < stats.total) {
1672
+ return phase;
1673
+ }
1674
+ }
1675
+ return phases[phases.length - 1] ?? 1;
1676
+ }
1677
+ function getRecentComplexity(tasks) {
1678
+ const doneTasks = tasks.filter((t) => t.status === "DONE").sort((a, b) => {
1679
+ const [aPhase, aOrder] = [parsePhase(a.taskId), parseTaskOrder(a.taskId)];
1680
+ const [bPhase, bOrder] = [parsePhase(b.taskId), parseTaskOrder(b.taskId)];
1681
+ if (aPhase !== bPhase) return bPhase - aPhase;
1682
+ return bOrder - aOrder;
1683
+ });
1684
+ return doneTasks[0]?.complexity ?? null;
1685
+ }
1686
+ function scoreTask(task, currentPhase, completedTaskIds, allTasks, recentComplexity) {
1687
+ const phase = parsePhase(task.taskId);
1688
+ const reasons = [];
1689
+ let score = 0;
1690
+ let phaseScore = 0;
1691
+ if (phase === currentPhase) {
1692
+ phaseScore = 100;
1693
+ reasons.push(`In current phase (Phase ${phase})`);
1694
+ } else if (phase === currentPhase + 1) {
1695
+ phaseScore = 50;
1696
+ reasons.push(`Next phase (Phase ${phase})`);
1697
+ } else if (phase < currentPhase) {
1698
+ phaseScore = 100;
1699
+ reasons.push(`Earlier incomplete phase (Phase ${phase})`);
1700
+ }
1701
+ score += phaseScore * 0.4;
1702
+ const unlocksCount = countDependentTasks(task.taskId, allTasks);
1703
+ const maxUnlocks = Math.max(
1704
+ 1,
1705
+ ...allTasks.map((t) => countDependentTasks(t.taskId, allTasks))
1706
+ );
1707
+ const dependencyScore = unlocksCount / maxUnlocks * 100;
1708
+ if (unlocksCount > 0) {
1709
+ reasons.push(`Unlocks ${unlocksCount} other task${unlocksCount > 1 ? "s" : ""}`);
1710
+ }
1711
+ score += dependencyScore * 0.3;
1712
+ let complexityScore = 50;
1713
+ if (recentComplexity) {
1714
+ if (recentComplexity === "High" && task.complexity !== "High") {
1715
+ complexityScore = 100;
1716
+ reasons.push("Good complexity balance after high-complexity task");
1717
+ } else if (recentComplexity === "Low" && task.complexity !== "Low") {
1718
+ complexityScore = 100;
1719
+ reasons.push("Good complexity progression");
1720
+ } else if (task.complexity === "Medium") {
1721
+ complexityScore = 80;
1722
+ }
1723
+ } else if (task.complexity === "Low") {
1724
+ complexityScore = 90;
1725
+ reasons.push("Quick win opportunity");
1726
+ }
1727
+ score += complexityScore * 0.2;
1728
+ const taskOrder = parseTaskOrder(task.taskId);
1729
+ const flowScore = Math.max(0, 100 - taskOrder * 10);
1730
+ if (taskOrder <= 2) {
1731
+ reasons.push("Sequential task order");
1732
+ }
1733
+ score += flowScore * 0.1;
1734
+ return {
1735
+ id: task.id,
1736
+ taskId: task.taskId,
1737
+ name: task.name,
1738
+ status: task.status,
1739
+ complexity: task.complexity,
1740
+ estimatedHours: task.estimatedHours,
1741
+ dependencies: task.dependencies,
1742
+ description: task.description,
1743
+ phase,
1744
+ score,
1745
+ reasons,
1746
+ unlocksCount
1747
+ };
1748
+ }
1749
+ var taskNextTool = {
1750
+ name: "planflow_task_next",
1751
+ description: `Get an intelligent recommendation for the next task to work on.
1752
+
1753
+ Analyzes project tasks and recommends the best next task based on:
1754
+ - Dependencies (prioritizes tasks that unlock others)
1755
+ - Phase progression (completes earlier phases first)
1756
+ - Complexity balance (prevents burnout, maintains momentum)
1757
+ - Sequential order (natural task flow)
1758
+
1759
+ Usage:
1760
+ planflow_task_next(projectId: "uuid")
1761
+
1762
+ Parameters:
1763
+ - projectId (required): The project UUID (get from planflow_projects)
1764
+
1765
+ Returns:
1766
+ - Recommended task with details
1767
+ - Reasoning for the recommendation
1768
+ - Alternative tasks if the main recommendation doesn't fit
1769
+ - Project progress overview
1770
+
1771
+ You must be logged in first with planflow_login.`,
1772
+ inputSchema: TaskNextInputSchema,
1773
+ async execute(input) {
1774
+ logger.info("Finding next task recommendation", { projectId: input.projectId });
1775
+ if (!isAuthenticated()) {
1776
+ logger.debug("No active session found");
1777
+ return createErrorResult(
1778
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
1779
+ );
1780
+ }
1781
+ try {
1782
+ const client = getApiClient();
1783
+ const response = await client.listTasks(input.projectId);
1784
+ logger.info("Successfully retrieved tasks for analysis", {
1785
+ projectId: input.projectId,
1786
+ count: response.tasks.length
1787
+ });
1788
+ const tasks = response.tasks;
1789
+ if (tasks.length === 0) {
1790
+ return createSuccessResult(
1791
+ `\u{1F4CB} No tasks found in project "${response.projectName}".
1792
+
1793
+ \u{1F4A1} Tasks are created when you sync your PROJECT_PLAN.md:
1794
+ planflow_sync(projectId: "${input.projectId}", direction: "push")`
1795
+ );
1796
+ }
1797
+ const completedTaskIds = new Set(
1798
+ tasks.filter((t) => t.status === "DONE").map((t) => t.taskId)
1799
+ );
1800
+ const currentPhase = findCurrentPhase(tasks);
1801
+ const recentComplexity = getRecentComplexity(tasks);
1802
+ const availableTasks = tasks.filter(
1803
+ (task) => task.status === "TODO" && areDependenciesSatisfied(task.dependencies, completedTaskIds)
1804
+ );
1805
+ const inProgressTasks = tasks.filter((t) => t.status === "IN_PROGRESS");
1806
+ const blockedTasks = tasks.filter((t) => t.status === "BLOCKED");
1807
+ const stats = {
1808
+ total: tasks.length,
1809
+ done: tasks.filter((t) => t.status === "DONE").length,
1810
+ inProgress: inProgressTasks.length,
1811
+ blocked: blockedTasks.length,
1812
+ todo: tasks.filter((t) => t.status === "TODO").length
1813
+ };
1814
+ const progressPercent = Math.round(stats.done / stats.total * 100);
1815
+ const progressBarLength = 10;
1816
+ const filledBlocks = Math.floor(progressPercent / 10);
1817
+ const progressBar = "\u{1F7E9}".repeat(filledBlocks) + "\u2B1C".repeat(progressBarLength - filledBlocks);
1818
+ if (stats.done === stats.total) {
1819
+ return createSuccessResult(
1820
+ `\u{1F389} Congratulations! All tasks completed!
1821
+
1822
+ \u2705 Project: ${response.projectName}
1823
+ \u{1F4CA} Progress: ${progressBar} 100%
1824
+ \u{1F3C6} ${stats.total} tasks completed
1825
+
1826
+ Project Status: \u2705 COMPLETE
1827
+
1828
+ \u{1F3AF} What's next?
1829
+ \u2022 Deploy to production (if not already)
1830
+ \u2022 Write post-mortem / lessons learned
1831
+ \u2022 Gather user feedback
1832
+ \u2022 Plan next version/features
1833
+ \u2022 Celebrate your success! \u{1F38A}
1834
+
1835
+ Great work on completing this project! \u{1F680}`
1836
+ );
1837
+ }
1838
+ if (availableTasks.length === 0) {
1839
+ let output2 = `\u26A0\uFE0F No tasks currently available to start.
1840
+
1841
+ \u{1F4CA} Project Status:
1842
+ ${progressBar} ${progressPercent}%
1843
+ \u2705 Completed: ${stats.done}/${stats.total}
1844
+ \u{1F504} In Progress: ${stats.inProgress}
1845
+ \u{1F6AB} Blocked: ${stats.blocked}
1846
+ \u23F3 Waiting on Dependencies: ${stats.todo - availableTasks.length}
1847
+ `;
1848
+ if (inProgressTasks.length > 0) {
1849
+ output2 += `
1850
+ \u{1F504} Tasks In Progress:
1851
+ `;
1852
+ for (const task of inProgressTasks) {
1853
+ output2 += ` \u2022 ${task.taskId}: ${task.name}
1854
+ `;
1855
+ }
1856
+ }
1857
+ if (blockedTasks.length > 0) {
1858
+ output2 += `
1859
+ \u{1F6AB} Blocked Tasks:
1860
+ `;
1861
+ for (const task of blockedTasks) {
1862
+ output2 += ` \u2022 ${task.taskId}: ${task.name}
1863
+ `;
1864
+ }
1865
+ }
1866
+ output2 += `
1867
+ \u{1F4A1} Suggested Actions:
1868
+ 1. Complete in-progress tasks
1869
+ 2. Resolve blockers on blocked tasks
1870
+ 3. Review dependencies if tasks seem stuck
1871
+
1872
+ \u{1F4A1} Commands:
1873
+ \u2022 planflow_task_list(projectId: "${input.projectId}", status: "IN_PROGRESS")
1874
+ \u2022 planflow_task_update(projectId: "${input.projectId}", taskId: "TX.Y", status: "DONE")`;
1875
+ return createSuccessResult(output2);
1876
+ }
1877
+ const scoredTasks = availableTasks.map(
1878
+ (task) => scoreTask(task, currentPhase, completedTaskIds, tasks, recentComplexity)
1879
+ ).sort((a, b) => b.score - a.score);
1880
+ const recommended = scoredTasks[0];
1881
+ const alternatives = scoredTasks.slice(1, 4);
1882
+ let inProgressWarning = "";
1883
+ if (inProgressTasks.length >= 3) {
1884
+ inProgressWarning = `\u26A0\uFE0F You have ${inProgressTasks.length} tasks in progress.
1885
+
1886
+ \u{1F4A1} Tip: Consider finishing in-progress tasks before starting new ones:
1887
+ `;
1888
+ for (const task of inProgressTasks.slice(0, 3)) {
1889
+ inProgressWarning += ` \u2022 ${task.taskId}: ${task.name}
1890
+ `;
1891
+ }
1892
+ inProgressWarning += `
1893
+ Benefits of finishing first:
1894
+ \u2022 Clear sense of progress
1895
+ \u2022 Unlock dependent tasks
1896
+ \u2022 Maintain focus and momentum
1897
+
1898
+ ${"\u2500".repeat(60)}
1899
+
1900
+ Still want to start something new? Here's the recommendation:
1901
+
1902
+ `;
1903
+ }
1904
+ const complexityIndicator = getComplexityIndicator3(recommended.complexity);
1905
+ let output = inProgressWarning + `\u{1F3AF} Recommended Next Task
1906
+
1907
+ ${recommended.taskId}: ${recommended.name}
1908
+
1909
+ ` + formatKeyValue({
1910
+ "Complexity": `${complexityIndicator} ${recommended.complexity}`,
1911
+ "Estimated": recommended.estimatedHours ? `${recommended.estimatedHours} hours` : "Not estimated",
1912
+ "Phase": `${recommended.phase}`,
1913
+ "Dependencies": recommended.dependencies.length > 0 ? `${recommended.dependencies.join(", ")} \u2705` : "None"
1914
+ }) + `
1915
+
1916
+ \u2705 All dependencies completed
1917
+ `;
1918
+ if (recommended.reasons.length > 0) {
1919
+ output += `
1920
+ \u{1F3AF} Why this task?
1921
+ `;
1922
+ for (const reason of recommended.reasons) {
1923
+ output += ` \u2022 ${reason}
1924
+ `;
1925
+ }
1926
+ }
1927
+ if (recommended.description) {
1928
+ output += `
1929
+ \u{1F4DD} Task Details:
1930
+ ${recommended.description}
1931
+ `;
1932
+ }
1933
+ output += `
1934
+ ${"\u2500".repeat(60)}
1935
+
1936
+ Ready to start?
1937
+ planflow_task_update(projectId: "${input.projectId}", taskId: "${recommended.taskId}", status: "IN_PROGRESS")
1938
+ `;
1939
+ if (alternatives.length > 0) {
1940
+ output += `
1941
+ ${"\u2500".repeat(60)}
1942
+
1943
+ `;
1944
+ output += `\u{1F4A1} Alternative Tasks (if this doesn't fit):
1945
+
1946
+ `;
1947
+ for (let i = 0; i < alternatives.length; i++) {
1948
+ const alt = alternatives[i];
1949
+ const altComplexity = getComplexityIndicator3(alt.complexity);
1950
+ output += `${i + 1}. ${alt.taskId}: ${alt.name}
1951
+ `;
1952
+ output += ` ${altComplexity} ${alt.complexity}`;
1953
+ if (alt.estimatedHours) {
1954
+ output += ` \u2022 ${alt.estimatedHours}h`;
1955
+ }
1956
+ if (alt.unlocksCount > 0) {
1957
+ output += ` \u2022 Unlocks ${alt.unlocksCount} task${alt.unlocksCount > 1 ? "s" : ""}`;
1958
+ }
1959
+ output += "\n";
1960
+ }
1961
+ }
1962
+ output += `
1963
+ ${"\u2500".repeat(60)}
1964
+
1965
+ `;
1966
+ output += `\u{1F4CA} Progress: ${progressBar} ${progressPercent}%
1967
+ `;
1968
+ output += `Total: ${stats.total} | \u2705 ${stats.done} | \u{1F504} ${stats.inProgress} | \u{1F4CB} ${stats.todo} | \u{1F6AB} ${stats.blocked}`;
1969
+ if (progressPercent < 25) {
1970
+ output += `
1971
+
1972
+ \u{1F31F} Early Stage Tips:
1973
+ \u2022 Focus on foundation tasks
1974
+ \u2022 Don't skip setup steps
1975
+ \u2022 Document as you go`;
1976
+ } else if (progressPercent >= 75) {
1977
+ output += `
1978
+
1979
+ \u{1F3C1} Final Sprint:
1980
+ \u2022 Almost there!
1981
+ \u2022 Don't rush quality
1982
+ \u2022 Test thoroughly`;
1983
+ }
1984
+ logger.info("Successfully generated task recommendation", {
1985
+ recommendedTask: recommended.taskId,
1986
+ score: recommended.score,
1987
+ alternativeCount: alternatives.length
1988
+ });
1989
+ return createSuccessResult(output);
1990
+ } catch (error) {
1991
+ logger.error("Failed to find next task", {
1992
+ error: String(error),
1993
+ projectId: input.projectId
1994
+ });
1995
+ if (error instanceof AuthError) {
1996
+ return createErrorResult(
1997
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
1998
+ );
1999
+ }
2000
+ if (error instanceof ApiError) {
2001
+ if (error.statusCode === 404) {
2002
+ return createErrorResult(
2003
+ `\u274C Project not found: ${input.projectId}
2004
+
2005
+ Please check the project ID and try again.
2006
+ Use planflow_projects() to list your available projects.`
2007
+ );
2008
+ }
2009
+ return createErrorResult(
2010
+ `\u274C API error: ${error.message}
2011
+
2012
+ Please check your internet connection and try again.`
2013
+ );
2014
+ }
2015
+ const message = error instanceof Error ? error.message : String(error);
2016
+ return createErrorResult(
2017
+ `\u274C Failed to find next task: ${message}
2018
+
2019
+ Please try again or check your connection.`
2020
+ );
2021
+ }
2022
+ }
2023
+ };
2024
+
2025
+ // src/tools/notifications.ts
2026
+ import { z as z11 } from "zod";
2027
+ var NotificationsInputSchema = z11.object({
2028
+ action: z11.enum(["list", "read", "read-all"]).default("list").describe("Action to perform: list, read (mark one as read), or read-all"),
2029
+ projectId: z11.string().uuid("Project ID must be a valid UUID").optional().describe("Filter notifications by project (optional)"),
2030
+ notificationId: z11.string().uuid("Notification ID must be a valid UUID").optional().describe('Notification ID to mark as read (required for "read" action)'),
2031
+ unreadOnly: z11.boolean().default(true).describe("Only show unread notifications (default: true)"),
2032
+ limit: z11.number().int().min(1).max(100).default(20).describe("Maximum number of notifications to fetch (default: 20)")
2033
+ });
2034
+ function getTypeEmoji(type) {
2035
+ switch (type) {
2036
+ case "comment":
2037
+ return "\u{1F4AC}";
2038
+ case "status_change":
2039
+ return "\u{1F504}";
2040
+ case "task_assigned":
2041
+ return "\u{1F464}";
2042
+ case "task_blocked":
2043
+ return "\u{1F6AB}";
2044
+ case "task_unblocked":
2045
+ return "\u2705";
2046
+ case "mention":
2047
+ return "\u{1F4E3}";
2048
+ default:
2049
+ return "\u{1F514}";
2050
+ }
2051
+ }
2052
+ function formatRelativeTime(dateString) {
2053
+ const date = new Date(dateString);
2054
+ const now = /* @__PURE__ */ new Date();
2055
+ const diffMs = now.getTime() - date.getTime();
2056
+ const diffMinutes = Math.floor(diffMs / 6e4);
2057
+ const diffHours = Math.floor(diffMinutes / 60);
2058
+ const diffDays = Math.floor(diffHours / 24);
2059
+ if (diffMinutes < 1) return "just now";
2060
+ if (diffMinutes < 60) return `${diffMinutes} min ago`;
2061
+ if (diffHours < 24) return `${diffHours}h ago`;
2062
+ if (diffDays < 7) return `${diffDays}d ago`;
2063
+ return date.toLocaleDateString();
2064
+ }
2065
+ function truncate3(str, maxLength) {
2066
+ if (!str) return "-";
2067
+ if (str.length <= maxLength) return str;
2068
+ return str.slice(0, maxLength - 3) + "...";
2069
+ }
2070
+ var notificationsTool = {
2071
+ name: "planflow_notifications",
2072
+ description: `View and manage PlanFlow notifications.
2073
+
2074
+ Get notified about task updates, comments, and team activities.
2075
+
2076
+ Usage:
2077
+ planflow_notifications() # List unread notifications
2078
+ planflow_notifications(unreadOnly: false) # List all notifications
2079
+ planflow_notifications(projectId: "uuid") # Filter by project
2080
+ planflow_notifications(action: "read", notificationId: "uuid") # Mark as read
2081
+ planflow_notifications(action: "read-all") # Mark all as read
2082
+
2083
+ Parameters:
2084
+ - action (optional): "list" (default), "read", or "read-all"
2085
+ - projectId (optional): Filter by project UUID
2086
+ - notificationId (required for "read"): Notification UUID to mark as read
2087
+ - unreadOnly (optional): Only show unread (default: true)
2088
+ - limit (optional): Max notifications to fetch (default: 20, max: 100)
2089
+
2090
+ Notification Types:
2091
+ - comment: Someone commented on a task
2092
+ - status_change: Task status was updated
2093
+ - task_assigned: You were assigned to a task
2094
+ - task_blocked: A task was blocked
2095
+ - task_unblocked: A task was unblocked
2096
+ - mention: You were mentioned in a comment
2097
+
2098
+ You must be logged in first with planflow_login.`,
2099
+ inputSchema: NotificationsInputSchema,
2100
+ async execute(input) {
2101
+ logger.info("Notifications tool called", { action: input.action, projectId: input.projectId });
2102
+ if (!isAuthenticated()) {
2103
+ logger.debug("No active session found");
2104
+ return createErrorResult(
2105
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
2106
+ );
2107
+ }
2108
+ try {
2109
+ const client = getApiClient();
2110
+ switch (input.action) {
2111
+ case "read": {
2112
+ if (!input.notificationId) {
2113
+ return createErrorResult(
2114
+ '\u274C Missing notificationId\n\nTo mark a notification as read, provide the notification ID:\n planflow_notifications(action: "read", notificationId: "uuid")\n\nUse planflow_notifications() to list notifications and get their IDs.'
2115
+ );
2116
+ }
2117
+ logger.info("Marking notification as read", { notificationId: input.notificationId });
2118
+ const result = await client.markNotificationRead(input.notificationId);
2119
+ return createSuccessResult(
2120
+ `\u2705 Notification marked as read
2121
+
2122
+ ${getTypeEmoji(result.notification.type)} ${result.notification.message}
2123
+
2124
+ \u{1F4A1} Commands:
2125
+ \u2022 planflow_notifications() - View remaining notifications`
2126
+ );
2127
+ }
2128
+ case "read-all": {
2129
+ logger.info("Marking all notifications as read", { projectId: input.projectId });
2130
+ const result = await client.markAllNotificationsRead(input.projectId);
2131
+ const scopeMessage = input.projectId ? "for this project" : "across all projects";
2132
+ return createSuccessResult(
2133
+ `\u2705 Marked ${result.markedCount} notification${result.markedCount !== 1 ? "s" : ""} as read ${scopeMessage}
2134
+
2135
+ \u{1F514} You're all caught up!
2136
+
2137
+ \u{1F4A1} Commands:
2138
+ \u2022 planflow_notifications(unreadOnly: false) - View all notifications`
2139
+ );
2140
+ }
2141
+ case "list":
2142
+ default: {
2143
+ logger.info("Fetching notifications", {
2144
+ projectId: input.projectId,
2145
+ unreadOnly: input.unreadOnly,
2146
+ limit: input.limit
2147
+ });
2148
+ const response = await client.listNotifications({
2149
+ projectId: input.projectId,
2150
+ unreadOnly: input.unreadOnly,
2151
+ limit: input.limit
2152
+ });
2153
+ logger.info("Successfully retrieved notifications", {
2154
+ count: response.notifications.length,
2155
+ unreadCount: response.unreadCount,
2156
+ totalCount: response.totalCount
2157
+ });
2158
+ if (response.notifications.length === 0) {
2159
+ const filterMessage = input.unreadOnly ? "unread " : "";
2160
+ const projectMessage = input.projectId ? " for this project" : "";
2161
+ return createSuccessResult(
2162
+ `\u{1F514} No ${filterMessage}notifications${projectMessage}
2163
+
2164
+ ` + (input.unreadOnly ? "\u2728 You're all caught up!\n\n\u{1F4A1} Commands:\n \u2022 planflow_notifications(unreadOnly: false) - View all notifications" : "\u{1F4A1} Notifications will appear when:\n \u2022 Someone comments on your tasks\n \u2022 Task statuses change\n \u2022 You're assigned to a task\n \u2022 You're mentioned in a comment")
2165
+ );
2166
+ }
2167
+ const headers = ["Type", "Message", "Project", "Task", "Time", "Read"];
2168
+ const rows = response.notifications.map((n) => [
2169
+ getTypeEmoji(n.type),
2170
+ truncate3(n.message, 35),
2171
+ truncate3(n.projectName, 15),
2172
+ n.taskId ?? "-",
2173
+ formatRelativeTime(n.createdAt),
2174
+ n.read ? "\u2713" : "\u2022"
2175
+ ]);
2176
+ const filterLabel = input.unreadOnly ? " (unread only)" : "";
2177
+ const projectLabel = input.projectId ? ` for project` : "";
2178
+ const output = [
2179
+ `\u{1F514} Notifications${filterLabel}${projectLabel}
2180
+ `,
2181
+ `Unread: ${response.unreadCount} | Total: ${response.totalCount}
2182
+ `,
2183
+ formatTable(headers, rows),
2184
+ "\n\n\u{1F4A1} Commands:",
2185
+ ' \u2022 planflow_notifications(action: "read", notificationId: "uuid") - Mark as read',
2186
+ ' \u2022 planflow_notifications(action: "read-all") - Mark all as read',
2187
+ input.unreadOnly ? " \u2022 planflow_notifications(unreadOnly: false) - Show all notifications" : " \u2022 planflow_notifications() - Show only unread"
2188
+ ].join("\n");
2189
+ return createSuccessResult(output);
2190
+ }
2191
+ }
2192
+ } catch (error) {
2193
+ logger.error("Failed to handle notifications", { error: String(error), action: input.action });
2194
+ if (error instanceof AuthError) {
2195
+ return createErrorResult(
2196
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
2197
+ );
2198
+ }
2199
+ if (error instanceof ApiError) {
2200
+ if (error.statusCode === 404) {
2201
+ if (input.action === "read") {
2202
+ return createErrorResult(
2203
+ `\u274C Notification not found: ${input.notificationId}
2204
+
2205
+ The notification may have been deleted or the ID is incorrect.
2206
+ Use planflow_notifications() to list available notifications.`
2207
+ );
2208
+ }
2209
+ return createErrorResult(
2210
+ `\u274C Resource not found
2211
+
2212
+ Please check the project ID and try again.
2213
+ Use planflow_projects() to list your available projects.`
2214
+ );
2215
+ }
2216
+ return createErrorResult(
2217
+ `\u274C API error: ${error.message}
2218
+
2219
+ Please check your internet connection and try again.`
2220
+ );
2221
+ }
2222
+ const message = error instanceof Error ? error.message : String(error);
2223
+ return createErrorResult(
2224
+ `\u274C Failed to handle notifications: ${message}
2225
+
2226
+ Please try again or check your connection.`
2227
+ );
2228
+ }
2229
+ }
2230
+ };
2231
+
2232
+ // src/tools/activity.ts
2233
+ import { z as z12 } from "zod";
2234
+ var ActivityInputSchema = z12.object({
2235
+ projectId: z12.string().uuid("Project ID must be a valid UUID").describe("Project ID to get activity for"),
2236
+ taskId: z12.string().optional().describe('Optional: Filter activity for a specific task (e.g., "T1.1")'),
2237
+ action: z12.enum([
2238
+ "task_created",
2239
+ "task_updated",
2240
+ "task_deleted",
2241
+ "task_status_changed",
2242
+ "task_assigned",
2243
+ "task_unassigned",
2244
+ "comment_created",
2245
+ "comment_updated",
2246
+ "comment_deleted",
2247
+ "project_updated",
2248
+ "plan_updated",
2249
+ "member_invited",
2250
+ "member_joined",
2251
+ "member_removed",
2252
+ "member_role_changed"
2253
+ ]).optional().describe("Optional: Filter by action type"),
2254
+ entityType: z12.enum(["task", "comment", "project", "member", "invitation"]).optional().describe("Optional: Filter by entity type"),
2255
+ limit: z12.number().int().min(1).max(100).default(20).describe("Maximum number of activities to fetch (default: 20)")
2256
+ });
2257
+ function getActionEmoji(action) {
2258
+ switch (action) {
2259
+ // Task actions
2260
+ case "task_created":
2261
+ return "\u2728";
2262
+ case "task_updated":
2263
+ return "\u{1F4DD}";
2264
+ case "task_deleted":
2265
+ return "\u{1F5D1}\uFE0F";
2266
+ case "task_status_changed":
2267
+ return "\u{1F504}";
2268
+ case "task_assigned":
2269
+ return "\u{1F464}";
2270
+ case "task_unassigned":
2271
+ return "\u{1F464}";
2272
+ // Comment actions
2273
+ case "comment_created":
2274
+ return "\u{1F4AC}";
2275
+ case "comment_updated":
2276
+ return "\u270F\uFE0F";
2277
+ case "comment_deleted":
2278
+ return "\u{1F5D1}\uFE0F";
2279
+ // Project actions
2280
+ case "project_created":
2281
+ return "\u{1F195}";
2282
+ case "project_updated":
2283
+ return "\u{1F4CB}";
2284
+ case "project_deleted":
2285
+ return "\u{1F5D1}\uFE0F";
2286
+ case "plan_updated":
2287
+ return "\u{1F4C4}";
2288
+ // Member actions
2289
+ case "member_invited":
2290
+ return "\u{1F4E8}";
2291
+ case "member_joined":
2292
+ return "\u{1F389}";
2293
+ case "member_removed":
2294
+ return "\u{1F44B}";
2295
+ case "member_role_changed":
2296
+ return "\u{1F511}";
2297
+ default:
2298
+ return "\u{1F4CC}";
2299
+ }
2300
+ }
2301
+ function formatActionName(action) {
2302
+ return action.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2303
+ }
2304
+ function formatRelativeTime2(dateString) {
2305
+ const date = new Date(dateString);
2306
+ const now = /* @__PURE__ */ new Date();
2307
+ const diffMs = now.getTime() - date.getTime();
2308
+ const diffMinutes = Math.floor(diffMs / 6e4);
2309
+ const diffHours = Math.floor(diffMinutes / 60);
2310
+ const diffDays = Math.floor(diffHours / 24);
2311
+ if (diffMinutes < 1) return "just now";
2312
+ if (diffMinutes < 60) return `${diffMinutes}m ago`;
2313
+ if (diffHours < 24) return `${diffHours}h ago`;
2314
+ if (diffDays < 7) return `${diffDays}d ago`;
2315
+ return date.toLocaleDateString();
2316
+ }
2317
+ function truncate4(str, maxLength) {
2318
+ if (!str) return "-";
2319
+ if (str.length <= maxLength) return str;
2320
+ return str.slice(0, maxLength - 3) + "...";
2321
+ }
2322
+ function formatActorName(name, email) {
2323
+ if (name) {
2324
+ const firstName = name.split(" ")[0];
2325
+ return firstName.length <= 12 ? firstName : truncate4(name, 12);
2326
+ }
2327
+ return truncate4(email.split("@")[0], 12);
2328
+ }
2329
+ var activityTool = {
2330
+ name: "planflow_activity",
2331
+ description: `View recent activity for a PlanFlow project.
2332
+
2333
+ See what's happening in your project - who did what and when.
2334
+
2335
+ Usage:
2336
+ planflow_activity(projectId: "uuid") # Recent project activity
2337
+ planflow_activity(projectId: "uuid", taskId: "T1.1") # Activity for specific task
2338
+ planflow_activity(projectId: "uuid", action: "task_status_changed") # Filter by action
2339
+ planflow_activity(projectId: "uuid", limit: 50) # Get more activities
2340
+
2341
+ Parameters:
2342
+ - projectId (required): Project UUID
2343
+ - taskId (optional): Filter by task (e.g., "T1.1", "T2.3")
2344
+ - action (optional): Filter by action type
2345
+ - entityType (optional): Filter by entity type (task, comment, project, member)
2346
+ - limit (optional): Max activities to fetch (default: 20, max: 100)
2347
+
2348
+ Action Types:
2349
+ Task: task_created, task_updated, task_status_changed, task_assigned, task_unassigned
2350
+ Comment: comment_created, comment_updated, comment_deleted
2351
+ Project: project_updated, plan_updated
2352
+ Team: member_invited, member_joined, member_removed, member_role_changed
2353
+
2354
+ Entity Types:
2355
+ task, comment, project, member, invitation
2356
+
2357
+ You must be logged in first with planflow_login.`,
2358
+ inputSchema: ActivityInputSchema,
2359
+ async execute(input) {
2360
+ logger.info("Activity tool called", {
2361
+ projectId: input.projectId,
2362
+ taskId: input.taskId,
2363
+ action: input.action
2364
+ });
2365
+ if (!isAuthenticated()) {
2366
+ logger.debug("No active session found");
2367
+ return createErrorResult(
2368
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
2369
+ );
2370
+ }
2371
+ try {
2372
+ const client = getApiClient();
2373
+ logger.info("Fetching activity", {
2374
+ projectId: input.projectId,
2375
+ taskId: input.taskId,
2376
+ action: input.action,
2377
+ entityType: input.entityType,
2378
+ limit: input.limit
2379
+ });
2380
+ const response = input.taskId ? await client.getTaskActivity(input.projectId, input.taskId, {
2381
+ action: input.action,
2382
+ limit: input.limit
2383
+ }) : await client.getProjectActivity(input.projectId, {
2384
+ action: input.action,
2385
+ entityType: input.entityType,
2386
+ taskId: input.taskId,
2387
+ limit: input.limit
2388
+ });
2389
+ logger.info("Successfully retrieved activity", {
2390
+ count: response.activities.length,
2391
+ total: response.pagination.total
2392
+ });
2393
+ if (input.taskId) {
2394
+ try {
2395
+ await client.startWorkingOn(input.projectId, input.taskId);
2396
+ logger.debug("Auto-started working on task from activity view", { taskId: input.taskId });
2397
+ } catch (workingOnError) {
2398
+ logger.debug("Failed to update working on status from activity view (non-fatal)", {
2399
+ error: String(workingOnError)
2400
+ });
2401
+ }
2402
+ }
2403
+ if (response.activities.length === 0) {
2404
+ const filterMessage = input.taskId ? ` for task ${input.taskId}` : input.action ? ` with action "${input.action}"` : "";
2405
+ return createSuccessResult(
2406
+ `\u{1F4CB} No activity found${filterMessage}
2407
+
2408
+ \u{1F4A1} Activity will appear when:
2409
+ \u2022 Tasks are created, updated, or completed
2410
+ \u2022 Comments are added
2411
+ \u2022 Team members join or are invited
2412
+ \u2022 The project plan is updated`
2413
+ );
2414
+ }
2415
+ const headers = ["", "Who", "Action", "Details", "When"];
2416
+ const rows = response.activities.map((a) => {
2417
+ let details = "";
2418
+ if (a.taskId) {
2419
+ details = a.taskId;
2420
+ }
2421
+ if (a.description) {
2422
+ details = details ? `${details}: ${truncate4(a.description, 25)}` : truncate4(a.description, 30);
2423
+ }
2424
+ if (!details && a.metadata) {
2425
+ const meta = a.metadata;
2426
+ if (meta.oldStatus && meta.newStatus) {
2427
+ details = `${meta.oldStatus} \u2192 ${meta.newStatus}`;
2428
+ } else if (meta.assigneeName) {
2429
+ details = `\u2192 ${meta.assigneeName}`;
2430
+ } else if (meta.inviteeEmail) {
2431
+ details = truncate4(String(meta.inviteeEmail), 20);
2432
+ }
2433
+ }
2434
+ return [
2435
+ getActionEmoji(a.action),
2436
+ formatActorName(a.actor.name, a.actor.email),
2437
+ formatActionName(a.action),
2438
+ details || "-",
2439
+ formatRelativeTime2(a.createdAt)
2440
+ ];
2441
+ });
2442
+ const titleParts = ["\u{1F4CB} Recent Activity"];
2443
+ if (input.taskId) {
2444
+ titleParts.push(`for ${input.taskId}`);
2445
+ }
2446
+ if (input.action) {
2447
+ titleParts.push(`(${formatActionName(input.action)})`);
2448
+ }
2449
+ const paginationInfo = response.pagination.hasMore ? `Showing ${response.activities.length} of ${response.pagination.total}` : `${response.activities.length} activities`;
2450
+ const output = [
2451
+ `${titleParts.join(" ")}
2452
+ `,
2453
+ `${paginationInfo}
2454
+ `,
2455
+ formatTable(headers, rows),
2456
+ "\n\n\u{1F4A1} Commands:",
2457
+ ' \u2022 planflow_activity(projectId: "...", taskId: "T1.1") - Task activity',
2458
+ ' \u2022 planflow_activity(projectId: "...", action: "task_status_changed") - Filter by action',
2459
+ ' \u2022 planflow_activity(projectId: "...", limit: 50) - Get more activities'
2460
+ ].join("\n");
2461
+ return createSuccessResult(output);
2462
+ } catch (error) {
2463
+ logger.error("Failed to get activity", { error: String(error) });
2464
+ if (error instanceof AuthError) {
2465
+ return createErrorResult(
2466
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
2467
+ );
2468
+ }
2469
+ if (error instanceof ApiError) {
2470
+ if (error.statusCode === 404) {
2471
+ return createErrorResult(
2472
+ `\u274C Project not found: ${input.projectId}
2473
+
2474
+ Please check the project ID and try again.
2475
+ Use planflow_projects() to list your available projects.`
2476
+ );
2477
+ }
2478
+ return createErrorResult(
2479
+ `\u274C API error: ${error.message}
2480
+
2481
+ Please check your internet connection and try again.`
2482
+ );
2483
+ }
2484
+ const message = error instanceof Error ? error.message : String(error);
2485
+ return createErrorResult(
2486
+ `\u274C Failed to get activity: ${message}
2487
+
2488
+ Please try again or check your connection.`
2489
+ );
2490
+ }
2491
+ }
2492
+ };
2493
+
2494
+ // src/tools/comments.ts
2495
+ import { z as z13 } from "zod";
2496
+ var CommentsInputSchema = z13.object({
2497
+ projectId: z13.string().uuid("Project ID must be a valid UUID").describe("Project ID containing the task"),
2498
+ taskId: z13.string().describe('Task ID to view comments for (e.g., "T1.1")')
2499
+ });
2500
+ function formatRelativeTime3(dateString) {
2501
+ const date = new Date(dateString);
2502
+ const now = /* @__PURE__ */ new Date();
2503
+ const diffMs = now.getTime() - date.getTime();
2504
+ const diffMinutes = Math.floor(diffMs / 6e4);
2505
+ const diffHours = Math.floor(diffMinutes / 60);
2506
+ const diffDays = Math.floor(diffHours / 24);
2507
+ if (diffMinutes < 1) return "just now";
2508
+ if (diffMinutes < 60) return `${diffMinutes}m ago`;
2509
+ if (diffHours < 24) return `${diffHours}h ago`;
2510
+ if (diffDays < 7) return `${diffDays}d ago`;
2511
+ return date.toLocaleDateString();
2512
+ }
2513
+ function formatAuthorName(name, email) {
2514
+ if (name) {
2515
+ const firstName = name.split(" ")[0];
2516
+ return firstName.length <= 15 ? firstName : name.slice(0, 12) + "...";
2517
+ }
2518
+ return email.split("@")[0].slice(0, 12);
2519
+ }
2520
+ function formatComment(comment, indent = 0) {
2521
+ const prefix = indent > 0 ? " ".repeat(indent) + "\u2514\u2500 " : "";
2522
+ const authorName = formatAuthorName(comment.author.name, comment.author.email);
2523
+ const time = formatRelativeTime3(comment.createdAt);
2524
+ const maxContentLength = 200 - indent * 2;
2525
+ const content = comment.content.length > maxContentLength ? comment.content.slice(0, maxContentLength - 3) + "..." : comment.content;
2526
+ const lines = [
2527
+ `${prefix}\u{1F4AC} ${authorName} (${time})`,
2528
+ `${prefix} ${content}`
2529
+ ];
2530
+ if (comment.replies && comment.replies.length > 0) {
2531
+ for (const reply of comment.replies) {
2532
+ lines.push(formatComment(reply, indent + 1));
2533
+ }
2534
+ }
2535
+ return lines.join("\n");
2536
+ }
2537
+ var commentsTool = {
2538
+ name: "planflow_comments",
2539
+ description: `View comments on a PlanFlow task.
2540
+
2541
+ See all comments and replies on a specific task.
2542
+
2543
+ Usage:
2544
+ planflow_comments(projectId: "uuid", taskId: "T1.1")
2545
+
2546
+ Parameters:
2547
+ - projectId (required): Project UUID
2548
+ - taskId (required): Task ID (e.g., "T1.1", "T2.3")
2549
+
2550
+ Output includes:
2551
+ - Comment content
2552
+ - Author name/email
2553
+ - Timestamp
2554
+ - Threaded replies
2555
+
2556
+ You must be logged in first with planflow_login.`,
2557
+ inputSchema: CommentsInputSchema,
2558
+ async execute(input) {
2559
+ logger.info("Comments tool called", {
2560
+ projectId: input.projectId,
2561
+ taskId: input.taskId
2562
+ });
2563
+ if (!isAuthenticated()) {
2564
+ logger.debug("No active session found");
2565
+ return createErrorResult(
2566
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
2567
+ );
2568
+ }
2569
+ try {
2570
+ const client = getApiClient();
2571
+ logger.info("Fetching comments", {
2572
+ projectId: input.projectId,
2573
+ taskId: input.taskId
2574
+ });
2575
+ const response = await client.listComments(input.projectId, input.taskId);
2576
+ logger.info("Successfully retrieved comments", {
2577
+ count: response.comments.length,
2578
+ total: response.totalCount
2579
+ });
2580
+ try {
2581
+ await client.startWorkingOn(input.projectId, input.taskId);
2582
+ logger.debug("Auto-started working on task from comments view", { taskId: input.taskId });
2583
+ } catch (workingOnError) {
2584
+ logger.debug("Failed to update working on status from comments view (non-fatal)", {
2585
+ error: String(workingOnError)
2586
+ });
2587
+ }
2588
+ if (response.comments.length === 0) {
2589
+ return createSuccessResult(
2590
+ `\u{1F4AC} No comments on task ${input.taskId}
2591
+
2592
+ \u{1F4A1} Add a comment using:
2593
+ planflow_comment(projectId: "${input.projectId}", taskId: "${input.taskId}", content: "Your comment")`
2594
+ );
2595
+ }
2596
+ const commentsList = response.comments.map((comment) => formatComment(comment)).join("\n\n");
2597
+ const output = [
2598
+ `\u{1F4AC} Comments on ${input.taskId}
2599
+ `,
2600
+ `${response.totalCount} comment${response.totalCount !== 1 ? "s" : ""}
2601
+ `,
2602
+ "\u2500".repeat(50),
2603
+ "",
2604
+ commentsList,
2605
+ "",
2606
+ "\u2500".repeat(50),
2607
+ "\n\u{1F4A1} Commands:",
2608
+ ` \u2022 Add comment: planflow_comment(projectId: "...", taskId: "${input.taskId}", content: "...")`,
2609
+ ` \u2022 Reply: planflow_comment(projectId: "...", taskId: "${input.taskId}", content: "...", parentId: "comment-id")`
2610
+ ].join("\n");
2611
+ return createSuccessResult(output);
2612
+ } catch (error) {
2613
+ logger.error("Failed to get comments", { error: String(error) });
2614
+ if (error instanceof AuthError) {
2615
+ return createErrorResult(
2616
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
2617
+ );
2618
+ }
2619
+ if (error instanceof ApiError) {
2620
+ if (error.statusCode === 404) {
2621
+ return createErrorResult(
2622
+ `\u274C Task not found: ${input.taskId}
2623
+
2624
+ Please check the task ID and try again.
2625
+ Use planflow_task_list(projectId: "...") to list available tasks.`
2626
+ );
2627
+ }
2628
+ return createErrorResult(
2629
+ `\u274C API error: ${error.message}
2630
+
2631
+ Please check your internet connection and try again.`
2632
+ );
2633
+ }
2634
+ const message = error instanceof Error ? error.message : String(error);
2635
+ return createErrorResult(
2636
+ `\u274C Failed to get comments: ${message}
2637
+
2638
+ Please try again or check your connection.`
2639
+ );
2640
+ }
2641
+ }
2642
+ };
2643
+
2644
+ // src/tools/comment.ts
2645
+ import { z as z14 } from "zod";
2646
+ var CommentInputSchema = z14.object({
2647
+ projectId: z14.string().uuid("Project ID must be a valid UUID").describe("Project ID containing the task"),
2648
+ taskId: z14.string().describe('Task ID to add comment to (e.g., "T1.1")'),
2649
+ content: z14.string().min(1, "Comment content is required").max(1e4, "Comment must be at most 10000 characters").describe("Comment content (supports @mentions like @user@email.com)"),
2650
+ parentId: z14.string().uuid("Parent comment ID must be a valid UUID").optional().describe("Optional: Reply to a specific comment by providing its ID")
2651
+ });
2652
+ function formatAuthorName2(name, email) {
2653
+ if (name) {
2654
+ return name;
2655
+ }
2656
+ return email.split("@")[0];
2657
+ }
2658
+ var commentTool = {
2659
+ name: "planflow_comment",
2660
+ description: `Add a comment to a PlanFlow task.
2661
+
2662
+ Post a comment on a specific task. Supports @mentions to notify team members.
2663
+
2664
+ Usage:
2665
+ planflow_comment(projectId: "uuid", taskId: "T1.1", content: "Great progress!")
2666
+ planflow_comment(projectId: "uuid", taskId: "T1.1", content: "@john@email.com please review")
2667
+ planflow_comment(projectId: "uuid", taskId: "T1.1", content: "I agree!", parentId: "comment-uuid")
2668
+
2669
+ Parameters:
2670
+ - projectId (required): Project UUID
2671
+ - taskId (required): Task ID (e.g., "T1.1", "T2.3")
2672
+ - content (required): Comment text (max 10000 chars)
2673
+ - parentId (optional): Comment ID to reply to
2674
+
2675
+ Features:
2676
+ - @mentions: Include @email to notify team members
2677
+ - Replies: Use parentId to create threaded replies
2678
+ - Notifications: Mentioned users and task assignee get notified
2679
+
2680
+ You must be logged in first with planflow_login.`,
2681
+ inputSchema: CommentInputSchema,
2682
+ async execute(input) {
2683
+ logger.info("Comment tool called", {
2684
+ projectId: input.projectId,
2685
+ taskId: input.taskId,
2686
+ contentLength: input.content.length,
2687
+ isReply: !!input.parentId
2688
+ });
2689
+ if (!isAuthenticated()) {
2690
+ logger.debug("No active session found");
2691
+ return createErrorResult(
2692
+ '\u274C Not logged in.\n\nPlease authenticate first using:\n planflow_login(token: "your-api-token")\n\nGet your token at: https://planflow.tools/settings/api-tokens'
2693
+ );
2694
+ }
2695
+ try {
2696
+ const client = getApiClient();
2697
+ logger.info("Creating comment", {
2698
+ projectId: input.projectId,
2699
+ taskId: input.taskId,
2700
+ parentId: input.parentId
2701
+ });
2702
+ const response = await client.createComment(input.projectId, input.taskId, {
2703
+ content: input.content,
2704
+ parentId: input.parentId
2705
+ });
2706
+ const comment = response.comment;
2707
+ const authorName = formatAuthorName2(comment.author.name, comment.author.email);
2708
+ logger.info("Successfully created comment", {
2709
+ commentId: comment.id,
2710
+ taskId: input.taskId
2711
+ });
2712
+ try {
2713
+ await client.startWorkingOn(input.projectId, input.taskId);
2714
+ logger.debug("Auto-started working on task from comment", { taskId: input.taskId });
2715
+ } catch (workingOnError) {
2716
+ logger.debug("Failed to update working on status from comment (non-fatal)", {
2717
+ error: String(workingOnError)
2718
+ });
2719
+ }
2720
+ const isReply = !!input.parentId;
2721
+ const actionWord = isReply ? "Reply" : "Comment";
2722
+ const mentionMatches = input.content.match(/@[\w.+-]+@[\w.-]+\.\w+/g);
2723
+ const mentionCount = mentionMatches ? mentionMatches.length : 0;
2724
+ const displayContent = input.content.length > 100 ? input.content.slice(0, 97) + "..." : input.content;
2725
+ const outputLines = [
2726
+ `\u2705 ${actionWord} added to ${input.taskId}
2727
+ `,
2728
+ "\u2500".repeat(50),
2729
+ "",
2730
+ `\u{1F4AC} ${authorName}:`,
2731
+ ` "${displayContent}"`,
2732
+ ""
2733
+ ];
2734
+ if (mentionCount > 0) {
2735
+ outputLines.push(
2736
+ `\u{1F4EC} ${mentionCount} user${mentionCount > 1 ? "s" : ""} mentioned and will be notified`
2737
+ );
2738
+ outputLines.push("");
2739
+ }
2740
+ outputLines.push("\u2500".repeat(50));
2741
+ outputLines.push("\n\u{1F4A1} Commands:");
2742
+ outputLines.push(` \u2022 View comments: planflow_comments(projectId: "...", taskId: "${input.taskId}")`);
2743
+ outputLines.push(` \u2022 Reply: planflow_comment(projectId: "...", taskId: "${input.taskId}", content: "...", parentId: "${comment.id}")`);
2744
+ return createSuccessResult(outputLines.join("\n"));
2745
+ } catch (error) {
2746
+ logger.error("Failed to create comment", { error: String(error) });
2747
+ if (error instanceof AuthError) {
2748
+ return createErrorResult(
2749
+ '\u274C Authentication error: Your session may have expired.\n\nPlease log out and log in again:\n 1. planflow_logout()\n 2. planflow_login(token: "your-new-token")\n\nGet a new token at: https://planflow.tools/settings/api-tokens'
2750
+ );
2751
+ }
2752
+ if (error instanceof ApiError) {
2753
+ if (error.statusCode === 404) {
2754
+ const message2 = error.message.toLowerCase();
2755
+ if (message2.includes("task")) {
2756
+ return createErrorResult(
2757
+ `\u274C Task not found: ${input.taskId}
2758
+
2759
+ Please check the task ID and try again.
2760
+ Use planflow_task_list(projectId: "...") to list available tasks.`
2761
+ );
2762
+ }
2763
+ if (input.parentId && message2.includes("comment")) {
2764
+ return createErrorResult(
2765
+ `\u274C Parent comment not found: ${input.parentId}
2766
+
2767
+ The comment you're trying to reply to doesn't exist.
2768
+ Use planflow_comments(projectId: "...", taskId: "${input.taskId}") to view existing comments.`
2769
+ );
2770
+ }
2771
+ return createErrorResult(
2772
+ `\u274C Project not found: ${input.projectId}
2773
+
2774
+ Please check the project ID and try again.
2775
+ Use planflow_projects() to list your available projects.`
2776
+ );
2777
+ }
2778
+ if (error.statusCode === 400) {
2779
+ return createErrorResult(
2780
+ `\u274C Invalid request: ${error.message}
2781
+
2782
+ Please check your input and try again.`
2783
+ );
2784
+ }
2785
+ return createErrorResult(
2786
+ `\u274C API error: ${error.message}
2787
+
2788
+ Please check your internet connection and try again.`
2789
+ );
2790
+ }
2791
+ const message = error instanceof Error ? error.message : String(error);
2792
+ return createErrorResult(
2793
+ `\u274C Failed to add comment: ${message}
2794
+
2795
+ Please try again or check your connection.`
2796
+ );
2797
+ }
2798
+ }
2799
+ };
2800
+
2801
+ // src/tools/index.ts
2802
+ var tools = [
2803
+ loginTool,
2804
+ logoutTool,
2805
+ whoamiTool,
2806
+ projectsTool,
2807
+ createTool,
2808
+ syncTool,
2809
+ taskListTool,
2810
+ taskUpdateTool,
2811
+ taskNextTool,
2812
+ notificationsTool,
2813
+ activityTool,
2814
+ commentsTool,
2815
+ commentTool
2816
+ ];
2817
+
2818
+ // src/server.ts
2819
+ function formatErrorResponse(error) {
2820
+ let message;
2821
+ if (error instanceof PlanFlowError) {
2822
+ message = `Error [${error.code}]: ${error.message}`;
2823
+ if (error.details) {
2824
+ message += `
2825
+ Details: ${JSON.stringify(error.details, null, 2)}`;
2826
+ }
2827
+ } else if (error instanceof Error) {
2828
+ message = `Error: ${error.message}`;
2829
+ } else {
2830
+ message = `Error: ${String(error)}`;
2831
+ }
2832
+ return {
2833
+ content: [{ type: "text", text: message }],
2834
+ isError: true
2835
+ };
2836
+ }
2837
+ function zodToJsonSchema(schema) {
2838
+ if ("shape" in schema && schema.shape) {
2839
+ const shape = schema.shape;
2840
+ const properties = {};
2841
+ const required = [];
2842
+ for (const [key, value] of Object.entries(shape)) {
2843
+ const zodField = value;
2844
+ let type = "string";
2845
+ const typeName = zodField._def?.typeName;
2846
+ if (typeName === "ZodNumber") type = "number";
2847
+ if (typeName === "ZodBoolean") type = "boolean";
2848
+ if (typeName === "ZodArray") type = "array";
2849
+ properties[key] = {
2850
+ type,
2851
+ description: zodField._def?.description
2852
+ };
2853
+ if (typeof zodField.isOptional !== "function" || !zodField.isOptional()) {
2854
+ required.push(key);
2855
+ }
2856
+ }
2857
+ return {
2858
+ type: "object",
2859
+ properties,
2860
+ required: required.length > 0 ? required : void 0
2861
+ };
2862
+ }
2863
+ return { type: "object", properties: {} };
2864
+ }
2865
+ function createServer() {
2866
+ const server = new Server(
2867
+ {
2868
+ name: APP_NAME,
2869
+ version: APP_VERSION
2870
+ },
2871
+ {
2872
+ capabilities: {
2873
+ tools: {}
2874
+ }
2875
+ }
2876
+ );
2877
+ server.setRequestHandler(ListToolsRequestSchema, async () => {
2878
+ logger.debug("Listing tools", { count: tools.length });
2879
+ return {
2880
+ tools: tools.map((tool) => ({
2881
+ name: tool.name,
2882
+ description: tool.description,
2883
+ inputSchema: zodToJsonSchema(tool.inputSchema)
2884
+ }))
2885
+ };
2886
+ });
2887
+ server.setRequestHandler(CallToolRequestSchema, async (request) => {
2888
+ const { name, arguments: args } = request.params;
2889
+ logger.info("Tool called", { name });
2890
+ const tool = tools.find((t) => t.name === name);
2891
+ if (!tool) {
2892
+ logger.warn("Unknown tool requested", { name });
2893
+ return formatErrorResponse(new ToolError(`Unknown tool: ${name}`, name));
2894
+ }
2895
+ try {
2896
+ const parseResult = tool.inputSchema.safeParse(args);
2897
+ if (!parseResult.success) {
2898
+ logger.warn("Tool input validation failed", {
2899
+ name,
2900
+ errors: parseResult.error.errors
2901
+ });
2902
+ return formatErrorResponse(
2903
+ new PlanFlowError(
2904
+ `Invalid input: ${parseResult.error.errors.map((e) => e.message).join(", ")}`,
2905
+ "VALIDATION_ERROR",
2906
+ { errors: parseResult.error.errors }
2907
+ )
2908
+ );
2909
+ }
2910
+ const result = await tool.execute(parseResult.data);
2911
+ logger.debug("Tool executed successfully", { name });
2912
+ return result;
2913
+ } catch (error) {
2914
+ logger.error("Tool execution failed", {
2915
+ name,
2916
+ error: error instanceof Error ? error.message : String(error)
2917
+ });
2918
+ return formatErrorResponse(error);
2919
+ }
2920
+ });
2921
+ return server;
2922
+ }
2923
+ async function startServer() {
2924
+ logger.info("Starting PlanFlow MCP Server", { version: APP_VERSION });
2925
+ const server = createServer();
2926
+ const transport = new StdioServerTransport();
2927
+ server.onerror = (error) => {
2928
+ logger.error("Server error", { error: String(error) });
2929
+ };
2930
+ const shutdown = async () => {
2931
+ logger.info("Shutting down server...");
2932
+ await server.close();
2933
+ process.exit(0);
2934
+ };
2935
+ process.on("SIGINT", shutdown);
2936
+ process.on("SIGTERM", shutdown);
2937
+ await server.connect(transport);
2938
+ logger.info("Server connected and ready");
2939
+ }
2940
+
2941
+ // src/index.ts
2942
+ if (process.env["PLANFLOW_DEBUG"] === "true") {
2943
+ logger.setLevel("debug");
2944
+ }
2945
+ startServer().catch((error) => {
2946
+ logger.error("Failed to start server", {
2947
+ error: error instanceof Error ? error.message : String(error)
2948
+ });
2949
+ process.exit(1);
2950
+ });