@planflow-tools/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/LICENSE +21 -0
- package/README.md +183 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2261 -0
- package/package.json +81 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,2261 @@
|
|
|
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
|
+
// Utility Methods
|
|
514
|
+
// ============================================================
|
|
515
|
+
/**
|
|
516
|
+
* Health check - verify API is reachable
|
|
517
|
+
*/
|
|
518
|
+
async healthCheck() {
|
|
519
|
+
try {
|
|
520
|
+
const response = await fetch(`${this.baseUrl}/health`, {
|
|
521
|
+
method: "GET",
|
|
522
|
+
signal: AbortSignal.timeout(5e3)
|
|
523
|
+
});
|
|
524
|
+
return response.ok;
|
|
525
|
+
} catch {
|
|
526
|
+
return false;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* Get API information
|
|
531
|
+
*/
|
|
532
|
+
async getApiInfo() {
|
|
533
|
+
const response = await fetch(`${this.baseUrl}/`, {
|
|
534
|
+
method: "GET",
|
|
535
|
+
signal: AbortSignal.timeout(5e3)
|
|
536
|
+
});
|
|
537
|
+
if (!response.ok) {
|
|
538
|
+
throw new ApiError("Failed to get API info", response.status);
|
|
539
|
+
}
|
|
540
|
+
return response.json();
|
|
541
|
+
}
|
|
542
|
+
};
|
|
543
|
+
var apiClient = null;
|
|
544
|
+
function getApiClient() {
|
|
545
|
+
if (!apiClient) {
|
|
546
|
+
apiClient = new ApiClient();
|
|
547
|
+
apiClient.loadToken();
|
|
548
|
+
}
|
|
549
|
+
return apiClient;
|
|
550
|
+
}
|
|
551
|
+
function resetApiClient() {
|
|
552
|
+
apiClient = null;
|
|
553
|
+
}
|
|
554
|
+
function createApiClient(options) {
|
|
555
|
+
return new ApiClient(options);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
// src/tools/types.ts
|
|
559
|
+
function createSuccessResult(text) {
|
|
560
|
+
return {
|
|
561
|
+
content: [{ type: "text", text }]
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function createErrorResult(text) {
|
|
565
|
+
return {
|
|
566
|
+
content: [{ type: "text", text }],
|
|
567
|
+
isError: true
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
function formatTable(headers, rows, options) {
|
|
571
|
+
const padding = options?.padding ?? 2;
|
|
572
|
+
const widths = headers.map(
|
|
573
|
+
(h, i) => Math.max(h.length, ...rows.map((r) => (r[i] ?? "").length))
|
|
574
|
+
);
|
|
575
|
+
const headerRow = headers.map((h, i) => h.padEnd(widths[i])).join(" ".repeat(padding));
|
|
576
|
+
const separator = widths.map((w) => "-".repeat(w)).join(" ".repeat(padding));
|
|
577
|
+
const dataRows = rows.map(
|
|
578
|
+
(row) => row.map((cell, i) => (cell ?? "").padEnd(widths[i])).join(" ".repeat(padding))
|
|
579
|
+
);
|
|
580
|
+
return [headerRow, separator, ...dataRows].join("\n");
|
|
581
|
+
}
|
|
582
|
+
function formatKeyValue(pairs) {
|
|
583
|
+
const maxKeyLength = Math.max(...Object.keys(pairs).map((k) => k.length));
|
|
584
|
+
return Object.entries(pairs).map(([key, value]) => `${key.padEnd(maxKeyLength)}: ${String(value)}`).join("\n");
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
// src/tools/login.ts
|
|
588
|
+
var LoginInputSchema = z2.object({
|
|
589
|
+
token: z2.string().min(1, "API token is required")
|
|
590
|
+
});
|
|
591
|
+
var loginTool = {
|
|
592
|
+
name: "planflow_login",
|
|
593
|
+
description: `Authenticate with PlanFlow using an API token.
|
|
594
|
+
|
|
595
|
+
Get your API token from the PlanFlow dashboard at https://planflow.tools/settings/api-tokens
|
|
596
|
+
|
|
597
|
+
Usage:
|
|
598
|
+
planflow_login(token: "your-api-token")
|
|
599
|
+
|
|
600
|
+
After successful login, you can use other PlanFlow tools to manage your projects and tasks.`,
|
|
601
|
+
inputSchema: LoginInputSchema,
|
|
602
|
+
async execute(input) {
|
|
603
|
+
const { token } = input;
|
|
604
|
+
logger.info("Attempting to authenticate with PlanFlow");
|
|
605
|
+
if (isAuthenticated()) {
|
|
606
|
+
const config = loadConfig();
|
|
607
|
+
logger.debug("User already authenticated", { email: config.userEmail });
|
|
608
|
+
return createSuccessResult(
|
|
609
|
+
`\u26A0\uFE0F Already logged in as ${config.userEmail}
|
|
610
|
+
|
|
611
|
+
To switch accounts, run planflow_logout first, then login with the new token.`
|
|
612
|
+
);
|
|
613
|
+
}
|
|
614
|
+
try {
|
|
615
|
+
const client = createApiClient();
|
|
616
|
+
logger.debug("Verifying API token");
|
|
617
|
+
const verifyResponse = await client.verifyToken(token);
|
|
618
|
+
saveConfig({
|
|
619
|
+
apiToken: token,
|
|
620
|
+
userId: verifyResponse.user.id,
|
|
621
|
+
userEmail: verifyResponse.user.email
|
|
622
|
+
});
|
|
623
|
+
resetApiClient();
|
|
624
|
+
logger.info("Successfully authenticated", { email: verifyResponse.user.email });
|
|
625
|
+
const output = [
|
|
626
|
+
"\u2705 Successfully logged in to PlanFlow!\n",
|
|
627
|
+
formatKeyValue({
|
|
628
|
+
"User": verifyResponse.user.name,
|
|
629
|
+
"Email": verifyResponse.user.email,
|
|
630
|
+
"Token": verifyResponse.tokenName
|
|
631
|
+
}),
|
|
632
|
+
"\n\n\u{1F389} You can now use PlanFlow tools:",
|
|
633
|
+
" \u2022 planflow_projects - List your projects",
|
|
634
|
+
" \u2022 planflow_create - Create a new project",
|
|
635
|
+
" \u2022 planflow_sync - Sync project plans",
|
|
636
|
+
" \u2022 planflow_task_list - View project tasks",
|
|
637
|
+
" \u2022 planflow_whoami - Show current user info"
|
|
638
|
+
].join("\n");
|
|
639
|
+
return createSuccessResult(output);
|
|
640
|
+
} catch (error) {
|
|
641
|
+
logger.error("Authentication failed", { error: String(error) });
|
|
642
|
+
if (error instanceof AuthError) {
|
|
643
|
+
return createErrorResult(
|
|
644
|
+
"\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"
|
|
645
|
+
);
|
|
646
|
+
}
|
|
647
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
648
|
+
return createErrorResult(
|
|
649
|
+
`\u274C Authentication failed: ${message}
|
|
650
|
+
|
|
651
|
+
Please check your internet connection and try again.`
|
|
652
|
+
);
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
// src/tools/logout.ts
|
|
658
|
+
import { z as z3 } from "zod";
|
|
659
|
+
var LogoutInputSchema = z3.object({});
|
|
660
|
+
var logoutTool = {
|
|
661
|
+
name: "planflow_logout",
|
|
662
|
+
description: `Log out from PlanFlow and clear stored credentials.
|
|
663
|
+
|
|
664
|
+
This will remove your API token from local storage. You will need to login again with planflow_login to use other PlanFlow tools.
|
|
665
|
+
|
|
666
|
+
Usage:
|
|
667
|
+
planflow_logout()
|
|
668
|
+
|
|
669
|
+
No parameters required.`,
|
|
670
|
+
inputSchema: LogoutInputSchema,
|
|
671
|
+
async execute(_input) {
|
|
672
|
+
logger.info("Attempting to logout from PlanFlow");
|
|
673
|
+
if (!isAuthenticated()) {
|
|
674
|
+
logger.debug("No active session found");
|
|
675
|
+
return createErrorResult(
|
|
676
|
+
"\u26A0\uFE0F Not currently logged in.\n\nUse planflow_login to authenticate first."
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
try {
|
|
680
|
+
const config = loadConfig();
|
|
681
|
+
const userEmail = config.userEmail ?? "unknown";
|
|
682
|
+
clearCredentials();
|
|
683
|
+
resetApiClient();
|
|
684
|
+
logger.info("Successfully logged out", { email: userEmail });
|
|
685
|
+
return createSuccessResult(
|
|
686
|
+
`\u2705 Successfully logged out from PlanFlow!
|
|
687
|
+
|
|
688
|
+
Goodbye, ${userEmail}!
|
|
689
|
+
|
|
690
|
+
\u{1F510} Your API token has been removed from local storage.
|
|
691
|
+
|
|
692
|
+
To login again, use:
|
|
693
|
+
planflow_login(token: "your-api-token")
|
|
694
|
+
|
|
695
|
+
Get your token at: https://planflow.tools/settings/api-tokens`
|
|
696
|
+
);
|
|
697
|
+
} catch (error) {
|
|
698
|
+
logger.error("Logout failed", { error: String(error) });
|
|
699
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
700
|
+
return createErrorResult(
|
|
701
|
+
`\u274C Logout failed: ${message}
|
|
702
|
+
|
|
703
|
+
Please try again or manually delete the config file at:
|
|
704
|
+
~/.config/planflow/config.json`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
};
|
|
709
|
+
|
|
710
|
+
// src/tools/whoami.ts
|
|
711
|
+
import { z as z4 } from "zod";
|
|
712
|
+
var WhoamiInputSchema = z4.object({});
|
|
713
|
+
function formatDate(date) {
|
|
714
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
715
|
+
return d.toLocaleDateString("en-US", {
|
|
716
|
+
year: "numeric",
|
|
717
|
+
month: "short",
|
|
718
|
+
day: "numeric",
|
|
719
|
+
hour: "2-digit",
|
|
720
|
+
minute: "2-digit"
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
var whoamiTool = {
|
|
724
|
+
name: "planflow_whoami",
|
|
725
|
+
description: `Show information about the currently authenticated PlanFlow user.
|
|
726
|
+
|
|
727
|
+
Displays your user profile including name, email, account creation date, and authentication method.
|
|
728
|
+
|
|
729
|
+
Usage:
|
|
730
|
+
planflow_whoami()
|
|
731
|
+
|
|
732
|
+
No parameters required. You must be logged in first with planflow_login.`,
|
|
733
|
+
inputSchema: WhoamiInputSchema,
|
|
734
|
+
async execute(_input) {
|
|
735
|
+
logger.info("Fetching current user information");
|
|
736
|
+
if (!isAuthenticated()) {
|
|
737
|
+
logger.debug("No active session found");
|
|
738
|
+
return createErrorResult(
|
|
739
|
+
'\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'
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
try {
|
|
743
|
+
const client = getApiClient();
|
|
744
|
+
const response = await client.getCurrentUser();
|
|
745
|
+
const { user, authType } = response;
|
|
746
|
+
const config = loadConfig();
|
|
747
|
+
logger.info("Successfully retrieved user info", { email: user.email });
|
|
748
|
+
const output = [
|
|
749
|
+
"\u{1F464} Current User\n",
|
|
750
|
+
formatKeyValue({
|
|
751
|
+
"Name": user.name,
|
|
752
|
+
"Email": user.email,
|
|
753
|
+
"User ID": user.id,
|
|
754
|
+
"Auth Type": authType === "api-token" ? "API Token" : "JWT",
|
|
755
|
+
"Created": formatDate(user.createdAt),
|
|
756
|
+
"Updated": formatDate(user.updatedAt)
|
|
757
|
+
}),
|
|
758
|
+
"\n\n\u{1F4CA} Session Info",
|
|
759
|
+
formatKeyValue({
|
|
760
|
+
"API URL": config.apiUrl,
|
|
761
|
+
"Status": "\u2705 Connected"
|
|
762
|
+
}),
|
|
763
|
+
"\n\n\u{1F4A1} Available commands:",
|
|
764
|
+
" \u2022 planflow_projects - List your projects",
|
|
765
|
+
" \u2022 planflow_create - Create a new project",
|
|
766
|
+
" \u2022 planflow_sync - Sync project plans",
|
|
767
|
+
" \u2022 planflow_logout - Log out"
|
|
768
|
+
].join("\n");
|
|
769
|
+
return createSuccessResult(output);
|
|
770
|
+
} catch (error) {
|
|
771
|
+
logger.error("Failed to fetch user info", { error: String(error) });
|
|
772
|
+
if (error instanceof AuthError) {
|
|
773
|
+
return createErrorResult(
|
|
774
|
+
'\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'
|
|
775
|
+
);
|
|
776
|
+
}
|
|
777
|
+
if (error instanceof ApiError) {
|
|
778
|
+
return createErrorResult(
|
|
779
|
+
`\u274C API error: ${error.message}
|
|
780
|
+
|
|
781
|
+
Please check your internet connection and try again.`
|
|
782
|
+
);
|
|
783
|
+
}
|
|
784
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
785
|
+
return createErrorResult(
|
|
786
|
+
`\u274C Failed to fetch user info: ${message}
|
|
787
|
+
|
|
788
|
+
Please try again or check your connection.`
|
|
789
|
+
);
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
};
|
|
793
|
+
|
|
794
|
+
// src/tools/projects.ts
|
|
795
|
+
import { z as z5 } from "zod";
|
|
796
|
+
var ProjectsInputSchema = z5.object({});
|
|
797
|
+
function formatDate2(date) {
|
|
798
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
799
|
+
return d.toLocaleDateString("en-US", {
|
|
800
|
+
year: "numeric",
|
|
801
|
+
month: "short",
|
|
802
|
+
day: "numeric"
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
function truncate(str, maxLength) {
|
|
806
|
+
if (!str) return "-";
|
|
807
|
+
if (str.length <= maxLength) return str;
|
|
808
|
+
return str.slice(0, maxLength - 3) + "...";
|
|
809
|
+
}
|
|
810
|
+
var projectsTool = {
|
|
811
|
+
name: "planflow_projects",
|
|
812
|
+
description: `List all your PlanFlow projects.
|
|
813
|
+
|
|
814
|
+
Displays a table of all projects with their names, descriptions, and creation dates.
|
|
815
|
+
|
|
816
|
+
Usage:
|
|
817
|
+
planflow_projects()
|
|
818
|
+
|
|
819
|
+
No parameters required. You must be logged in first with planflow_login.
|
|
820
|
+
|
|
821
|
+
Returns:
|
|
822
|
+
- Project ID (use for other commands)
|
|
823
|
+
- Project name
|
|
824
|
+
- Description (truncated)
|
|
825
|
+
- Created date
|
|
826
|
+
- Updated date`,
|
|
827
|
+
inputSchema: ProjectsInputSchema,
|
|
828
|
+
async execute(_input) {
|
|
829
|
+
logger.info("Fetching projects list");
|
|
830
|
+
if (!isAuthenticated()) {
|
|
831
|
+
logger.debug("No active session found");
|
|
832
|
+
return createErrorResult(
|
|
833
|
+
'\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'
|
|
834
|
+
);
|
|
835
|
+
}
|
|
836
|
+
try {
|
|
837
|
+
const client = getApiClient();
|
|
838
|
+
const projects = await client.listProjects();
|
|
839
|
+
logger.info("Successfully retrieved projects", { count: projects.length });
|
|
840
|
+
if (projects.length === 0) {
|
|
841
|
+
return createSuccessResult(
|
|
842
|
+
`\u{1F4C1} No projects found.
|
|
843
|
+
|
|
844
|
+
You don't have any projects yet.
|
|
845
|
+
|
|
846
|
+
\u{1F4A1} Create your first project:
|
|
847
|
+
planflow_create(name: "My Project", description: "Optional description")
|
|
848
|
+
|
|
849
|
+
Or create one at: https://planflow.tools/projects/new`
|
|
850
|
+
);
|
|
851
|
+
}
|
|
852
|
+
const headers = ["ID", "Name", "Description", "Created", "Updated"];
|
|
853
|
+
const rows = projects.map((project) => [
|
|
854
|
+
project.id.slice(0, 8) + "...",
|
|
855
|
+
// Show first 8 chars of UUID
|
|
856
|
+
truncate(project.name, 25),
|
|
857
|
+
truncate(project.description, 30),
|
|
858
|
+
formatDate2(project.createdAt),
|
|
859
|
+
formatDate2(project.updatedAt)
|
|
860
|
+
]);
|
|
861
|
+
const output = [
|
|
862
|
+
`\u{1F4C1} Your Projects (${projects.length})
|
|
863
|
+
`,
|
|
864
|
+
formatTable(headers, rows),
|
|
865
|
+
"\n\n\u{1F4A1} Commands:",
|
|
866
|
+
' \u2022 planflow_sync(projectId: "...") - Sync project plan',
|
|
867
|
+
' \u2022 planflow_task_list(projectId: "...") - List project tasks',
|
|
868
|
+
' \u2022 planflow_create(name: "...") - Create new project',
|
|
869
|
+
"\n\u{1F4CB} Full project IDs:",
|
|
870
|
+
...projects.map((p) => ` \u2022 ${p.name}: ${p.id}`)
|
|
871
|
+
].join("\n");
|
|
872
|
+
return createSuccessResult(output);
|
|
873
|
+
} catch (error) {
|
|
874
|
+
logger.error("Failed to fetch projects", { error: String(error) });
|
|
875
|
+
if (error instanceof AuthError) {
|
|
876
|
+
return createErrorResult(
|
|
877
|
+
'\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'
|
|
878
|
+
);
|
|
879
|
+
}
|
|
880
|
+
if (error instanceof ApiError) {
|
|
881
|
+
return createErrorResult(
|
|
882
|
+
`\u274C API error: ${error.message}
|
|
883
|
+
|
|
884
|
+
Please check your internet connection and try again.`
|
|
885
|
+
);
|
|
886
|
+
}
|
|
887
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
888
|
+
return createErrorResult(
|
|
889
|
+
`\u274C Failed to fetch projects: ${message}
|
|
890
|
+
|
|
891
|
+
Please try again or check your connection.`
|
|
892
|
+
);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
// src/tools/create.ts
|
|
898
|
+
import { z as z6 } from "zod";
|
|
899
|
+
var CreateInputSchema = z6.object({
|
|
900
|
+
name: z6.string().min(1, "Project name is required").max(255, "Project name must be at most 255 characters"),
|
|
901
|
+
description: z6.string().max(1e3, "Description must be at most 1000 characters").optional()
|
|
902
|
+
});
|
|
903
|
+
function formatDate3(date) {
|
|
904
|
+
const d = typeof date === "string" ? new Date(date) : date;
|
|
905
|
+
return d.toLocaleDateString("en-US", {
|
|
906
|
+
year: "numeric",
|
|
907
|
+
month: "short",
|
|
908
|
+
day: "numeric",
|
|
909
|
+
hour: "2-digit",
|
|
910
|
+
minute: "2-digit"
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
var createTool = {
|
|
914
|
+
name: "planflow_create",
|
|
915
|
+
description: `Create a new PlanFlow project.
|
|
916
|
+
|
|
917
|
+
Creates a new project with the specified name and optional description.
|
|
918
|
+
|
|
919
|
+
Usage:
|
|
920
|
+
planflow_create(name: "My Project")
|
|
921
|
+
planflow_create(name: "My Project", description: "A description of my project")
|
|
922
|
+
|
|
923
|
+
Parameters:
|
|
924
|
+
- name (required): Project name (1-255 characters)
|
|
925
|
+
- description (optional): Project description (max 1000 characters)
|
|
926
|
+
|
|
927
|
+
You must be logged in first with planflow_login.
|
|
928
|
+
|
|
929
|
+
Returns:
|
|
930
|
+
- Project ID (use for sync and task commands)
|
|
931
|
+
- Project name
|
|
932
|
+
- Description
|
|
933
|
+
- Created timestamp`,
|
|
934
|
+
inputSchema: CreateInputSchema,
|
|
935
|
+
async execute(input) {
|
|
936
|
+
logger.info("Creating new project", { name: input.name });
|
|
937
|
+
if (!isAuthenticated()) {
|
|
938
|
+
logger.debug("No active session found");
|
|
939
|
+
return createErrorResult(
|
|
940
|
+
'\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'
|
|
941
|
+
);
|
|
942
|
+
}
|
|
943
|
+
try {
|
|
944
|
+
const client = getApiClient();
|
|
945
|
+
const project = await client.createProject({
|
|
946
|
+
name: input.name,
|
|
947
|
+
description: input.description
|
|
948
|
+
});
|
|
949
|
+
logger.info("Successfully created project", { projectId: project.id });
|
|
950
|
+
const output = [
|
|
951
|
+
"\u2705 Project created successfully!\n",
|
|
952
|
+
formatKeyValue({
|
|
953
|
+
"Project ID": project.id,
|
|
954
|
+
"Name": project.name,
|
|
955
|
+
"Description": project.description || "(none)",
|
|
956
|
+
"Created": formatDate3(project.createdAt)
|
|
957
|
+
}),
|
|
958
|
+
"\n\n\u{1F4A1} Next steps:",
|
|
959
|
+
' \u2022 planflow_sync(projectId: "' + project.id + '", direction: "push") - Upload your PROJECT_PLAN.md',
|
|
960
|
+
' \u2022 planflow_task_list(projectId: "' + project.id + '") - View project tasks',
|
|
961
|
+
" \u2022 planflow_projects() - List all projects",
|
|
962
|
+
"\n\u{1F4CB} Save this project ID for future commands:",
|
|
963
|
+
` ${project.id}`
|
|
964
|
+
].join("\n");
|
|
965
|
+
return createSuccessResult(output);
|
|
966
|
+
} catch (error) {
|
|
967
|
+
logger.error("Failed to create project", { error: String(error) });
|
|
968
|
+
if (error instanceof AuthError) {
|
|
969
|
+
return createErrorResult(
|
|
970
|
+
'\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'
|
|
971
|
+
);
|
|
972
|
+
}
|
|
973
|
+
if (error instanceof ApiError) {
|
|
974
|
+
if (error.statusCode === 400) {
|
|
975
|
+
return createErrorResult(
|
|
976
|
+
`\u274C Invalid project data: ${error.message}
|
|
977
|
+
|
|
978
|
+
Please check:
|
|
979
|
+
\u2022 Name is between 1-255 characters
|
|
980
|
+
\u2022 Description is at most 1000 characters`
|
|
981
|
+
);
|
|
982
|
+
}
|
|
983
|
+
return createErrorResult(
|
|
984
|
+
`\u274C API error: ${error.message}
|
|
985
|
+
|
|
986
|
+
Please check your internet connection and try again.`
|
|
987
|
+
);
|
|
988
|
+
}
|
|
989
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
990
|
+
return createErrorResult(
|
|
991
|
+
`\u274C Failed to create project: ${message}
|
|
992
|
+
|
|
993
|
+
Please try again or check your connection.`
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
// src/tools/sync.ts
|
|
1000
|
+
import { z as z7 } from "zod";
|
|
1001
|
+
var SyncInputSchema = z7.object({
|
|
1002
|
+
projectId: z7.string().uuid("Invalid project ID format"),
|
|
1003
|
+
direction: z7.enum(["push", "pull"]),
|
|
1004
|
+
content: z7.string().optional()
|
|
1005
|
+
// Required for push, ignored for pull
|
|
1006
|
+
});
|
|
1007
|
+
function formatDate4(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
|
+
function countLines(content) {
|
|
1018
|
+
if (!content) return 0;
|
|
1019
|
+
return content.split("\n").length;
|
|
1020
|
+
}
|
|
1021
|
+
function formatBytes(bytes) {
|
|
1022
|
+
if (bytes < 1024) return `${bytes} bytes`;
|
|
1023
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
1024
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
1025
|
+
}
|
|
1026
|
+
var syncTool = {
|
|
1027
|
+
name: "planflow_sync",
|
|
1028
|
+
description: `Sync PROJECT_PLAN.md with PlanFlow cloud.
|
|
1029
|
+
|
|
1030
|
+
Bidirectional synchronization between local plan files and the cloud.
|
|
1031
|
+
|
|
1032
|
+
Usage:
|
|
1033
|
+
planflow_sync(projectId: "uuid", direction: "push", content: "# Plan...") - Upload local plan
|
|
1034
|
+
planflow_sync(projectId: "uuid", direction: "pull") - Download cloud plan
|
|
1035
|
+
|
|
1036
|
+
Parameters:
|
|
1037
|
+
- projectId (required): Project UUID from planflow_create or planflow_projects
|
|
1038
|
+
- direction (required): "push" to upload, "pull" to download
|
|
1039
|
+
- content (required for push): The markdown content to upload
|
|
1040
|
+
|
|
1041
|
+
You must be logged in first with planflow_login.
|
|
1042
|
+
|
|
1043
|
+
Returns:
|
|
1044
|
+
- Push: Confirmation with size and timestamp
|
|
1045
|
+
- Pull: Full plan content (markdown) to save locally`,
|
|
1046
|
+
inputSchema: SyncInputSchema,
|
|
1047
|
+
async execute(input) {
|
|
1048
|
+
logger.info("Syncing project plan", {
|
|
1049
|
+
projectId: input.projectId,
|
|
1050
|
+
direction: input.direction
|
|
1051
|
+
});
|
|
1052
|
+
if (!isAuthenticated()) {
|
|
1053
|
+
logger.debug("No active session found");
|
|
1054
|
+
return createErrorResult(
|
|
1055
|
+
'\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'
|
|
1056
|
+
);
|
|
1057
|
+
}
|
|
1058
|
+
if (input.direction === "push" && !input.content) {
|
|
1059
|
+
logger.debug("Push requested without content");
|
|
1060
|
+
return createErrorResult(
|
|
1061
|
+
`\u274C Content is required for push operation.
|
|
1062
|
+
|
|
1063
|
+
Please provide the plan content:
|
|
1064
|
+
planflow_sync(
|
|
1065
|
+
projectId: "${input.projectId}",
|
|
1066
|
+
direction: "push",
|
|
1067
|
+
content: "# Your plan content here..."
|
|
1068
|
+
)
|
|
1069
|
+
|
|
1070
|
+
\u{1F4A1} Tip: Read your PROJECT_PLAN.md file and pass its content.`
|
|
1071
|
+
);
|
|
1072
|
+
}
|
|
1073
|
+
try {
|
|
1074
|
+
const client = getApiClient();
|
|
1075
|
+
if (input.direction === "push") {
|
|
1076
|
+
return await executePush(client, input.projectId, input.content);
|
|
1077
|
+
} else {
|
|
1078
|
+
return await executePull(client, input.projectId);
|
|
1079
|
+
}
|
|
1080
|
+
} catch (error) {
|
|
1081
|
+
logger.error("Failed to sync project plan", { error: String(error) });
|
|
1082
|
+
if (error instanceof AuthError) {
|
|
1083
|
+
return createErrorResult(
|
|
1084
|
+
'\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'
|
|
1085
|
+
);
|
|
1086
|
+
}
|
|
1087
|
+
if (error instanceof ApiError) {
|
|
1088
|
+
if (error.statusCode === 404) {
|
|
1089
|
+
return createErrorResult(
|
|
1090
|
+
"\u274C Project not found.\n\nPlease check the project ID and try again.\nRun planflow_projects() to list your available projects."
|
|
1091
|
+
);
|
|
1092
|
+
}
|
|
1093
|
+
return createErrorResult(
|
|
1094
|
+
`\u274C API error: ${error.message}
|
|
1095
|
+
|
|
1096
|
+
Please check your internet connection and try again.`
|
|
1097
|
+
);
|
|
1098
|
+
}
|
|
1099
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1100
|
+
return createErrorResult(
|
|
1101
|
+
`\u274C Failed to sync plan: ${message}
|
|
1102
|
+
|
|
1103
|
+
Please try again or check your connection.`
|
|
1104
|
+
);
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
};
|
|
1108
|
+
async function executePush(client, projectId, content) {
|
|
1109
|
+
logger.info("Pushing plan to cloud", { projectId, contentLength: content.length });
|
|
1110
|
+
const response = await client.updateProjectPlan(projectId, content);
|
|
1111
|
+
const bytes = new TextEncoder().encode(content).length;
|
|
1112
|
+
const lines = countLines(content);
|
|
1113
|
+
const output = [
|
|
1114
|
+
"\u2705 Plan synced to cloud!\n",
|
|
1115
|
+
formatKeyValue({
|
|
1116
|
+
"Project": response.projectName,
|
|
1117
|
+
"Direction": "push",
|
|
1118
|
+
"Size": `${formatBytes(bytes)} (${lines} lines)`,
|
|
1119
|
+
"Updated": formatDate4(response.updatedAt)
|
|
1120
|
+
}),
|
|
1121
|
+
"\n\n\u{1F4A1} Tip: Your local changes are now saved to the cloud."
|
|
1122
|
+
].join("\n");
|
|
1123
|
+
logger.info("Successfully pushed plan", { projectId });
|
|
1124
|
+
return createSuccessResult(output);
|
|
1125
|
+
}
|
|
1126
|
+
async function executePull(client, projectId) {
|
|
1127
|
+
logger.info("Pulling plan from cloud", { projectId });
|
|
1128
|
+
const response = await client.getProjectPlan(projectId);
|
|
1129
|
+
if (!response.plan) {
|
|
1130
|
+
const output2 = [
|
|
1131
|
+
"\u26A0\uFE0F No plan exists for this project yet.\n",
|
|
1132
|
+
formatKeyValue({
|
|
1133
|
+
"Project": response.projectName,
|
|
1134
|
+
"Project ID": response.projectId
|
|
1135
|
+
}),
|
|
1136
|
+
"\n\n\u{1F4A1} Tip: Create a PROJECT_PLAN.md locally and use 'push' to upload it."
|
|
1137
|
+
].join("\n");
|
|
1138
|
+
return createSuccessResult(output2);
|
|
1139
|
+
}
|
|
1140
|
+
const bytes = new TextEncoder().encode(response.plan).length;
|
|
1141
|
+
const lines = countLines(response.plan);
|
|
1142
|
+
const output = [
|
|
1143
|
+
"\u2705 Plan retrieved from cloud!\n",
|
|
1144
|
+
formatKeyValue({
|
|
1145
|
+
"Project": response.projectName,
|
|
1146
|
+
"Direction": "pull",
|
|
1147
|
+
"Size": `${formatBytes(bytes)} (${lines} lines)`,
|
|
1148
|
+
"Updated": formatDate4(response.updatedAt)
|
|
1149
|
+
}),
|
|
1150
|
+
"\n\n---",
|
|
1151
|
+
response.plan,
|
|
1152
|
+
"---",
|
|
1153
|
+
"\n\u{1F4A1} Tip: Save this content to PROJECT_PLAN.md in your project."
|
|
1154
|
+
].join("\n");
|
|
1155
|
+
logger.info("Successfully pulled plan", { projectId, contentLength: response.plan.length });
|
|
1156
|
+
return createSuccessResult(output);
|
|
1157
|
+
}
|
|
1158
|
+
|
|
1159
|
+
// src/tools/task-list.ts
|
|
1160
|
+
import { z as z8 } from "zod";
|
|
1161
|
+
var TaskListInputSchema = z8.object({
|
|
1162
|
+
projectId: z8.string().uuid("Project ID must be a valid UUID"),
|
|
1163
|
+
status: z8.enum(["TODO", "IN_PROGRESS", "DONE", "BLOCKED"]).optional().describe("Filter tasks by status")
|
|
1164
|
+
});
|
|
1165
|
+
function truncate2(str, maxLength) {
|
|
1166
|
+
if (!str) return "-";
|
|
1167
|
+
if (str.length <= maxLength) return str;
|
|
1168
|
+
return str.slice(0, maxLength - 3) + "...";
|
|
1169
|
+
}
|
|
1170
|
+
function getStatusEmoji(status) {
|
|
1171
|
+
switch (status) {
|
|
1172
|
+
case "TODO":
|
|
1173
|
+
return "\u{1F4CB}";
|
|
1174
|
+
case "IN_PROGRESS":
|
|
1175
|
+
return "\u{1F504}";
|
|
1176
|
+
case "DONE":
|
|
1177
|
+
return "\u2705";
|
|
1178
|
+
case "BLOCKED":
|
|
1179
|
+
return "\u{1F6AB}";
|
|
1180
|
+
default:
|
|
1181
|
+
return "\u2753";
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
function getComplexityIndicator(complexity) {
|
|
1185
|
+
switch (complexity) {
|
|
1186
|
+
case "Low":
|
|
1187
|
+
return "\u{1F7E2}";
|
|
1188
|
+
case "Medium":
|
|
1189
|
+
return "\u{1F7E1}";
|
|
1190
|
+
case "High":
|
|
1191
|
+
return "\u{1F534}";
|
|
1192
|
+
default:
|
|
1193
|
+
return "\u26AA";
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
var taskListTool = {
|
|
1197
|
+
name: "planflow_task_list",
|
|
1198
|
+
description: `List all tasks for a PlanFlow project.
|
|
1199
|
+
|
|
1200
|
+
Displays a table of tasks with their status, complexity, and dependencies.
|
|
1201
|
+
|
|
1202
|
+
Usage:
|
|
1203
|
+
planflow_task_list(projectId: "uuid")
|
|
1204
|
+
planflow_task_list(projectId: "uuid", status: "TODO")
|
|
1205
|
+
|
|
1206
|
+
Parameters:
|
|
1207
|
+
- projectId (required): The project UUID (get from planflow_projects)
|
|
1208
|
+
- status (optional): Filter by status - TODO, IN_PROGRESS, DONE, or BLOCKED
|
|
1209
|
+
|
|
1210
|
+
Returns:
|
|
1211
|
+
- Task ID (e.g., T1.1)
|
|
1212
|
+
- Task name
|
|
1213
|
+
- Status with emoji
|
|
1214
|
+
- Complexity indicator
|
|
1215
|
+
- Estimated hours
|
|
1216
|
+
- Dependencies
|
|
1217
|
+
|
|
1218
|
+
You must be logged in first with planflow_login.`,
|
|
1219
|
+
inputSchema: TaskListInputSchema,
|
|
1220
|
+
async execute(input) {
|
|
1221
|
+
logger.info("Fetching tasks list", { projectId: input.projectId, status: input.status });
|
|
1222
|
+
if (!isAuthenticated()) {
|
|
1223
|
+
logger.debug("No active session found");
|
|
1224
|
+
return createErrorResult(
|
|
1225
|
+
'\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'
|
|
1226
|
+
);
|
|
1227
|
+
}
|
|
1228
|
+
try {
|
|
1229
|
+
const client = getApiClient();
|
|
1230
|
+
const response = await client.listTasks(input.projectId);
|
|
1231
|
+
logger.info("Successfully retrieved tasks", {
|
|
1232
|
+
projectId: input.projectId,
|
|
1233
|
+
count: response.tasks.length
|
|
1234
|
+
});
|
|
1235
|
+
let tasks = response.tasks;
|
|
1236
|
+
if (input.status) {
|
|
1237
|
+
tasks = tasks.filter((t) => t.status === input.status);
|
|
1238
|
+
logger.debug("Filtered tasks by status", {
|
|
1239
|
+
status: input.status,
|
|
1240
|
+
filteredCount: tasks.length
|
|
1241
|
+
});
|
|
1242
|
+
}
|
|
1243
|
+
if (tasks.length === 0) {
|
|
1244
|
+
const filterMessage = input.status ? ` with status "${input.status}"` : "";
|
|
1245
|
+
return createSuccessResult(
|
|
1246
|
+
`\u{1F4CB} No tasks found${filterMessage}.
|
|
1247
|
+
|
|
1248
|
+
Project: ${response.projectName}
|
|
1249
|
+
|
|
1250
|
+
` + (input.status ? `\u{1F4A1} Try removing the status filter to see all tasks:
|
|
1251
|
+
planflow_task_list(projectId: "${input.projectId}")` : `\u{1F4A1} Tasks are created when you sync your PROJECT_PLAN.md:
|
|
1252
|
+
planflow_sync(projectId: "${input.projectId}", direction: "push")`)
|
|
1253
|
+
);
|
|
1254
|
+
}
|
|
1255
|
+
tasks.sort((a, b) => {
|
|
1256
|
+
const parseTaskId = (id) => {
|
|
1257
|
+
const match = id.match(/T(\d+)\.(\d+)/);
|
|
1258
|
+
if (!match) return [0, 0];
|
|
1259
|
+
return [parseInt(match[1], 10), parseInt(match[2], 10)];
|
|
1260
|
+
};
|
|
1261
|
+
const [aMajor, aMinor] = parseTaskId(a.taskId);
|
|
1262
|
+
const [bMajor, bMinor] = parseTaskId(b.taskId);
|
|
1263
|
+
if (aMajor !== bMajor) return aMajor - bMajor;
|
|
1264
|
+
return aMinor - bMinor;
|
|
1265
|
+
});
|
|
1266
|
+
const stats = {
|
|
1267
|
+
total: response.tasks.length,
|
|
1268
|
+
todo: response.tasks.filter((t) => t.status === "TODO").length,
|
|
1269
|
+
inProgress: response.tasks.filter((t) => t.status === "IN_PROGRESS").length,
|
|
1270
|
+
done: response.tasks.filter((t) => t.status === "DONE").length,
|
|
1271
|
+
blocked: response.tasks.filter((t) => t.status === "BLOCKED").length
|
|
1272
|
+
};
|
|
1273
|
+
const progressPercent = Math.round(stats.done / stats.total * 100);
|
|
1274
|
+
const headers = ["ID", "Name", "Status", "Complexity", "Est.", "Dependencies"];
|
|
1275
|
+
const rows = tasks.map((task) => [
|
|
1276
|
+
task.taskId,
|
|
1277
|
+
truncate2(task.name, 30),
|
|
1278
|
+
`${getStatusEmoji(task.status)} ${task.status}`,
|
|
1279
|
+
`${getComplexityIndicator(task.complexity)} ${task.complexity}`,
|
|
1280
|
+
task.estimatedHours ? `${task.estimatedHours}h` : "-",
|
|
1281
|
+
task.dependencies.length > 0 ? task.dependencies.join(", ") : "-"
|
|
1282
|
+
]);
|
|
1283
|
+
const progressBarLength = 10;
|
|
1284
|
+
const filledBlocks = Math.floor(progressPercent / 10);
|
|
1285
|
+
const progressBar = "\u{1F7E9}".repeat(filledBlocks) + "\u2B1C".repeat(progressBarLength - filledBlocks);
|
|
1286
|
+
const filterLabel = input.status ? ` (filtered: ${input.status})` : "";
|
|
1287
|
+
const output = [
|
|
1288
|
+
`\u{1F4CB} Tasks for "${response.projectName}"${filterLabel}
|
|
1289
|
+
`,
|
|
1290
|
+
`Progress: ${progressBar} ${progressPercent}%`,
|
|
1291
|
+
`Total: ${stats.total} | \u2705 ${stats.done} | \u{1F504} ${stats.inProgress} | \u{1F4CB} ${stats.todo} | \u{1F6AB} ${stats.blocked}
|
|
1292
|
+
`,
|
|
1293
|
+
formatTable(headers, rows),
|
|
1294
|
+
"\n\n\u{1F4A1} Commands:",
|
|
1295
|
+
` \u2022 planflow_task_update(projectId: "${input.projectId}", taskId: "T1.1", status: "DONE")`,
|
|
1296
|
+
` \u2022 planflow_task_list(projectId: "${input.projectId}", status: "TODO")`,
|
|
1297
|
+
` \u2022 planflow_sync(projectId: "${input.projectId}", direction: "pull")`
|
|
1298
|
+
].join("\n");
|
|
1299
|
+
return createSuccessResult(output);
|
|
1300
|
+
} catch (error) {
|
|
1301
|
+
logger.error("Failed to fetch tasks", { error: String(error), projectId: input.projectId });
|
|
1302
|
+
if (error instanceof AuthError) {
|
|
1303
|
+
return createErrorResult(
|
|
1304
|
+
'\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'
|
|
1305
|
+
);
|
|
1306
|
+
}
|
|
1307
|
+
if (error instanceof ApiError) {
|
|
1308
|
+
if (error.statusCode === 404) {
|
|
1309
|
+
return createErrorResult(
|
|
1310
|
+
`\u274C Project not found: ${input.projectId}
|
|
1311
|
+
|
|
1312
|
+
Please check the project ID and try again.
|
|
1313
|
+
Use planflow_projects() to list your available projects.`
|
|
1314
|
+
);
|
|
1315
|
+
}
|
|
1316
|
+
return createErrorResult(
|
|
1317
|
+
`\u274C API error: ${error.message}
|
|
1318
|
+
|
|
1319
|
+
Please check your internet connection and try again.`
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1322
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1323
|
+
return createErrorResult(
|
|
1324
|
+
`\u274C Failed to fetch tasks: ${message}
|
|
1325
|
+
|
|
1326
|
+
Please try again or check your connection.`
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
};
|
|
1331
|
+
|
|
1332
|
+
// src/tools/task-update.ts
|
|
1333
|
+
import { z as z9 } from "zod";
|
|
1334
|
+
var TaskUpdateInputSchema = z9.object({
|
|
1335
|
+
projectId: z9.string().uuid("Project ID must be a valid UUID"),
|
|
1336
|
+
taskId: z9.string().describe('Task ID (e.g., "T1.1", "T2.3")'),
|
|
1337
|
+
status: z9.enum(["TODO", "IN_PROGRESS", "DONE", "BLOCKED"]).describe("New status for the task")
|
|
1338
|
+
});
|
|
1339
|
+
function getStatusEmoji2(status) {
|
|
1340
|
+
switch (status) {
|
|
1341
|
+
case "TODO":
|
|
1342
|
+
return "\u{1F4CB}";
|
|
1343
|
+
case "IN_PROGRESS":
|
|
1344
|
+
return "\u{1F504}";
|
|
1345
|
+
case "DONE":
|
|
1346
|
+
return "\u2705";
|
|
1347
|
+
case "BLOCKED":
|
|
1348
|
+
return "\u{1F6AB}";
|
|
1349
|
+
default:
|
|
1350
|
+
return "\u2753";
|
|
1351
|
+
}
|
|
1352
|
+
}
|
|
1353
|
+
function getComplexityIndicator2(complexity) {
|
|
1354
|
+
switch (complexity) {
|
|
1355
|
+
case "Low":
|
|
1356
|
+
return "\u{1F7E2}";
|
|
1357
|
+
case "Medium":
|
|
1358
|
+
return "\u{1F7E1}";
|
|
1359
|
+
case "High":
|
|
1360
|
+
return "\u{1F534}";
|
|
1361
|
+
default:
|
|
1362
|
+
return "\u26AA";
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
var taskUpdateTool = {
|
|
1366
|
+
name: "planflow_task_update",
|
|
1367
|
+
description: `Update task status in a PlanFlow project.
|
|
1368
|
+
|
|
1369
|
+
Changes the status of a specific task (e.g., mark as done, in progress, or blocked).
|
|
1370
|
+
|
|
1371
|
+
Usage:
|
|
1372
|
+
planflow_task_update(projectId: "uuid", taskId: "T1.1", status: "IN_PROGRESS")
|
|
1373
|
+
planflow_task_update(projectId: "uuid", taskId: "T2.3", status: "DONE")
|
|
1374
|
+
|
|
1375
|
+
Parameters:
|
|
1376
|
+
- projectId (required): The project UUID (get from planflow_projects)
|
|
1377
|
+
- taskId (required): The task ID (e.g., "T1.1", "T2.3")
|
|
1378
|
+
- status (required): New status - TODO, IN_PROGRESS, DONE, or BLOCKED
|
|
1379
|
+
|
|
1380
|
+
Status meanings:
|
|
1381
|
+
- TODO: Task not yet started
|
|
1382
|
+
- IN_PROGRESS: Currently working on this task
|
|
1383
|
+
- DONE: Task completed
|
|
1384
|
+
- BLOCKED: Task cannot proceed (document blocker reason)
|
|
1385
|
+
|
|
1386
|
+
You must be logged in first with planflow_login.`,
|
|
1387
|
+
inputSchema: TaskUpdateInputSchema,
|
|
1388
|
+
async execute(input) {
|
|
1389
|
+
logger.info("Updating task", {
|
|
1390
|
+
projectId: input.projectId,
|
|
1391
|
+
taskId: input.taskId,
|
|
1392
|
+
status: input.status
|
|
1393
|
+
});
|
|
1394
|
+
if (!isAuthenticated()) {
|
|
1395
|
+
logger.debug("No active session found");
|
|
1396
|
+
return createErrorResult(
|
|
1397
|
+
'\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'
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
try {
|
|
1401
|
+
const client = getApiClient();
|
|
1402
|
+
const updatedTask = await client.updateTaskStatus(
|
|
1403
|
+
input.projectId,
|
|
1404
|
+
input.taskId,
|
|
1405
|
+
input.status
|
|
1406
|
+
);
|
|
1407
|
+
if (!updatedTask) {
|
|
1408
|
+
return createErrorResult(
|
|
1409
|
+
`\u274C Failed to update task ${input.taskId}.
|
|
1410
|
+
|
|
1411
|
+
The task may not exist or the update was rejected.
|
|
1412
|
+
Use planflow_task_list() to see available tasks.`
|
|
1413
|
+
);
|
|
1414
|
+
}
|
|
1415
|
+
logger.info("Successfully updated task", {
|
|
1416
|
+
taskId: input.taskId,
|
|
1417
|
+
newStatus: input.status
|
|
1418
|
+
});
|
|
1419
|
+
const statusEmoji = getStatusEmoji2(updatedTask.status);
|
|
1420
|
+
const complexityIndicator = getComplexityIndicator2(updatedTask.complexity);
|
|
1421
|
+
const taskDetails = formatKeyValue({
|
|
1422
|
+
"Task ID": updatedTask.taskId,
|
|
1423
|
+
"Name": updatedTask.name,
|
|
1424
|
+
"Status": `${statusEmoji} ${updatedTask.status}`,
|
|
1425
|
+
"Complexity": `${complexityIndicator} ${updatedTask.complexity}`,
|
|
1426
|
+
"Estimated": updatedTask.estimatedHours ? `${updatedTask.estimatedHours}h` : "-",
|
|
1427
|
+
"Dependencies": updatedTask.dependencies.length > 0 ? updatedTask.dependencies.join(", ") : "None"
|
|
1428
|
+
});
|
|
1429
|
+
let nextSteps;
|
|
1430
|
+
switch (input.status) {
|
|
1431
|
+
case "IN_PROGRESS":
|
|
1432
|
+
nextSteps = [
|
|
1433
|
+
"\n\u{1F4A1} Next steps:",
|
|
1434
|
+
` \u2022 When finished: planflow_task_update(projectId: "${input.projectId}", taskId: "${input.taskId}", status: "DONE")`,
|
|
1435
|
+
` \u2022 If blocked: planflow_task_update(projectId: "${input.projectId}", taskId: "${input.taskId}", status: "BLOCKED")`,
|
|
1436
|
+
` \u2022 Sync changes: planflow_sync(projectId: "${input.projectId}", direction: "pull")`
|
|
1437
|
+
].join("\n");
|
|
1438
|
+
break;
|
|
1439
|
+
case "DONE":
|
|
1440
|
+
nextSteps = [
|
|
1441
|
+
"\n\u{1F389} Great work!",
|
|
1442
|
+
"\n\u{1F4A1} Next steps:",
|
|
1443
|
+
` \u2022 Find next task: planflow_task_list(projectId: "${input.projectId}", status: "TODO")`,
|
|
1444
|
+
` \u2022 Sync changes: planflow_sync(projectId: "${input.projectId}", direction: "pull")`
|
|
1445
|
+
].join("\n");
|
|
1446
|
+
break;
|
|
1447
|
+
case "BLOCKED":
|
|
1448
|
+
nextSteps = [
|
|
1449
|
+
"\n\u26A0\uFE0F Task blocked - document the blocker:",
|
|
1450
|
+
" \u2022 What is blocking this task?",
|
|
1451
|
+
" \u2022 What needs to happen to unblock it?",
|
|
1452
|
+
" \u2022 Who can help resolve this?",
|
|
1453
|
+
"\n\u{1F4A1} When unblocked:",
|
|
1454
|
+
` \u2022 planflow_task_update(projectId: "${input.projectId}", taskId: "${input.taskId}", status: "IN_PROGRESS")`
|
|
1455
|
+
].join("\n");
|
|
1456
|
+
break;
|
|
1457
|
+
default:
|
|
1458
|
+
nextSteps = [
|
|
1459
|
+
"\n\u{1F4A1} Commands:",
|
|
1460
|
+
` \u2022 Start task: planflow_task_update(projectId: "${input.projectId}", taskId: "${input.taskId}", status: "IN_PROGRESS")`,
|
|
1461
|
+
` \u2022 View all tasks: planflow_task_list(projectId: "${input.projectId}")`
|
|
1462
|
+
].join("\n");
|
|
1463
|
+
}
|
|
1464
|
+
const output = [
|
|
1465
|
+
`${statusEmoji} Task ${input.taskId} updated to ${input.status}!
|
|
1466
|
+
`,
|
|
1467
|
+
taskDetails,
|
|
1468
|
+
nextSteps
|
|
1469
|
+
].join("\n");
|
|
1470
|
+
return createSuccessResult(output);
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
logger.error("Failed to update task", {
|
|
1473
|
+
error: String(error),
|
|
1474
|
+
projectId: input.projectId,
|
|
1475
|
+
taskId: input.taskId
|
|
1476
|
+
});
|
|
1477
|
+
if (error instanceof AuthError) {
|
|
1478
|
+
return createErrorResult(
|
|
1479
|
+
'\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'
|
|
1480
|
+
);
|
|
1481
|
+
}
|
|
1482
|
+
if (error instanceof ApiError) {
|
|
1483
|
+
if (error.statusCode === 404) {
|
|
1484
|
+
return createErrorResult(
|
|
1485
|
+
`\u274C Task not found: ${input.taskId}
|
|
1486
|
+
|
|
1487
|
+
Please check the task ID and try again.
|
|
1488
|
+
Use planflow_task_list(projectId: "${input.projectId}") to see available tasks.`
|
|
1489
|
+
);
|
|
1490
|
+
}
|
|
1491
|
+
return createErrorResult(
|
|
1492
|
+
`\u274C API error: ${error.message}
|
|
1493
|
+
|
|
1494
|
+
Please check your internet connection and try again.`
|
|
1495
|
+
);
|
|
1496
|
+
}
|
|
1497
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1498
|
+
return createErrorResult(
|
|
1499
|
+
`\u274C Failed to update task: ${message}
|
|
1500
|
+
|
|
1501
|
+
Please try again or check your connection.`
|
|
1502
|
+
);
|
|
1503
|
+
}
|
|
1504
|
+
}
|
|
1505
|
+
};
|
|
1506
|
+
|
|
1507
|
+
// src/tools/task-next.ts
|
|
1508
|
+
import { z as z10 } from "zod";
|
|
1509
|
+
var TaskNextInputSchema = z10.object({
|
|
1510
|
+
projectId: z10.string().uuid("Project ID must be a valid UUID")
|
|
1511
|
+
});
|
|
1512
|
+
function getComplexityIndicator3(complexity) {
|
|
1513
|
+
switch (complexity) {
|
|
1514
|
+
case "Low":
|
|
1515
|
+
return "\u{1F7E2}";
|
|
1516
|
+
case "Medium":
|
|
1517
|
+
return "\u{1F7E1}";
|
|
1518
|
+
case "High":
|
|
1519
|
+
return "\u{1F534}";
|
|
1520
|
+
default:
|
|
1521
|
+
return "\u26AA";
|
|
1522
|
+
}
|
|
1523
|
+
}
|
|
1524
|
+
function parsePhase(taskId) {
|
|
1525
|
+
const match = taskId.match(/T(\d+)\./);
|
|
1526
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
1527
|
+
}
|
|
1528
|
+
function parseTaskOrder(taskId) {
|
|
1529
|
+
const match = taskId.match(/T\d+\.(\d+)/);
|
|
1530
|
+
return match ? parseInt(match[1], 10) : 0;
|
|
1531
|
+
}
|
|
1532
|
+
function areDependenciesSatisfied(taskDependencies, completedTaskIds) {
|
|
1533
|
+
return taskDependencies.every((dep) => completedTaskIds.has(dep));
|
|
1534
|
+
}
|
|
1535
|
+
function countDependentTasks(taskId, allTasks) {
|
|
1536
|
+
return allTasks.filter((t) => t.dependencies.includes(taskId)).length;
|
|
1537
|
+
}
|
|
1538
|
+
function findCurrentPhase(tasks) {
|
|
1539
|
+
const phaseMap = /* @__PURE__ */ new Map();
|
|
1540
|
+
for (const task of tasks) {
|
|
1541
|
+
const phase = parsePhase(task.taskId);
|
|
1542
|
+
if (!phaseMap.has(phase)) {
|
|
1543
|
+
phaseMap.set(phase, { total: 0, done: 0 });
|
|
1544
|
+
}
|
|
1545
|
+
const stats = phaseMap.get(phase);
|
|
1546
|
+
stats.total++;
|
|
1547
|
+
if (task.status === "DONE") {
|
|
1548
|
+
stats.done++;
|
|
1549
|
+
}
|
|
1550
|
+
}
|
|
1551
|
+
const phases = Array.from(phaseMap.keys()).sort((a, b) => a - b);
|
|
1552
|
+
for (const phase of phases) {
|
|
1553
|
+
const stats = phaseMap.get(phase);
|
|
1554
|
+
if (stats.done < stats.total) {
|
|
1555
|
+
return phase;
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
return phases[phases.length - 1] ?? 1;
|
|
1559
|
+
}
|
|
1560
|
+
function getRecentComplexity(tasks) {
|
|
1561
|
+
const doneTasks = tasks.filter((t) => t.status === "DONE").sort((a, b) => {
|
|
1562
|
+
const [aPhase, aOrder] = [parsePhase(a.taskId), parseTaskOrder(a.taskId)];
|
|
1563
|
+
const [bPhase, bOrder] = [parsePhase(b.taskId), parseTaskOrder(b.taskId)];
|
|
1564
|
+
if (aPhase !== bPhase) return bPhase - aPhase;
|
|
1565
|
+
return bOrder - aOrder;
|
|
1566
|
+
});
|
|
1567
|
+
return doneTasks[0]?.complexity ?? null;
|
|
1568
|
+
}
|
|
1569
|
+
function scoreTask(task, currentPhase, completedTaskIds, allTasks, recentComplexity) {
|
|
1570
|
+
const phase = parsePhase(task.taskId);
|
|
1571
|
+
const reasons = [];
|
|
1572
|
+
let score = 0;
|
|
1573
|
+
let phaseScore = 0;
|
|
1574
|
+
if (phase === currentPhase) {
|
|
1575
|
+
phaseScore = 100;
|
|
1576
|
+
reasons.push(`In current phase (Phase ${phase})`);
|
|
1577
|
+
} else if (phase === currentPhase + 1) {
|
|
1578
|
+
phaseScore = 50;
|
|
1579
|
+
reasons.push(`Next phase (Phase ${phase})`);
|
|
1580
|
+
} else if (phase < currentPhase) {
|
|
1581
|
+
phaseScore = 100;
|
|
1582
|
+
reasons.push(`Earlier incomplete phase (Phase ${phase})`);
|
|
1583
|
+
}
|
|
1584
|
+
score += phaseScore * 0.4;
|
|
1585
|
+
const unlocksCount = countDependentTasks(task.taskId, allTasks);
|
|
1586
|
+
const maxUnlocks = Math.max(
|
|
1587
|
+
1,
|
|
1588
|
+
...allTasks.map((t) => countDependentTasks(t.taskId, allTasks))
|
|
1589
|
+
);
|
|
1590
|
+
const dependencyScore = unlocksCount / maxUnlocks * 100;
|
|
1591
|
+
if (unlocksCount > 0) {
|
|
1592
|
+
reasons.push(`Unlocks ${unlocksCount} other task${unlocksCount > 1 ? "s" : ""}`);
|
|
1593
|
+
}
|
|
1594
|
+
score += dependencyScore * 0.3;
|
|
1595
|
+
let complexityScore = 50;
|
|
1596
|
+
if (recentComplexity) {
|
|
1597
|
+
if (recentComplexity === "High" && task.complexity !== "High") {
|
|
1598
|
+
complexityScore = 100;
|
|
1599
|
+
reasons.push("Good complexity balance after high-complexity task");
|
|
1600
|
+
} else if (recentComplexity === "Low" && task.complexity !== "Low") {
|
|
1601
|
+
complexityScore = 100;
|
|
1602
|
+
reasons.push("Good complexity progression");
|
|
1603
|
+
} else if (task.complexity === "Medium") {
|
|
1604
|
+
complexityScore = 80;
|
|
1605
|
+
}
|
|
1606
|
+
} else if (task.complexity === "Low") {
|
|
1607
|
+
complexityScore = 90;
|
|
1608
|
+
reasons.push("Quick win opportunity");
|
|
1609
|
+
}
|
|
1610
|
+
score += complexityScore * 0.2;
|
|
1611
|
+
const taskOrder = parseTaskOrder(task.taskId);
|
|
1612
|
+
const flowScore = Math.max(0, 100 - taskOrder * 10);
|
|
1613
|
+
if (taskOrder <= 2) {
|
|
1614
|
+
reasons.push("Sequential task order");
|
|
1615
|
+
}
|
|
1616
|
+
score += flowScore * 0.1;
|
|
1617
|
+
return {
|
|
1618
|
+
id: task.id,
|
|
1619
|
+
taskId: task.taskId,
|
|
1620
|
+
name: task.name,
|
|
1621
|
+
status: task.status,
|
|
1622
|
+
complexity: task.complexity,
|
|
1623
|
+
estimatedHours: task.estimatedHours,
|
|
1624
|
+
dependencies: task.dependencies,
|
|
1625
|
+
description: task.description,
|
|
1626
|
+
phase,
|
|
1627
|
+
score,
|
|
1628
|
+
reasons,
|
|
1629
|
+
unlocksCount
|
|
1630
|
+
};
|
|
1631
|
+
}
|
|
1632
|
+
var taskNextTool = {
|
|
1633
|
+
name: "planflow_task_next",
|
|
1634
|
+
description: `Get an intelligent recommendation for the next task to work on.
|
|
1635
|
+
|
|
1636
|
+
Analyzes project tasks and recommends the best next task based on:
|
|
1637
|
+
- Dependencies (prioritizes tasks that unlock others)
|
|
1638
|
+
- Phase progression (completes earlier phases first)
|
|
1639
|
+
- Complexity balance (prevents burnout, maintains momentum)
|
|
1640
|
+
- Sequential order (natural task flow)
|
|
1641
|
+
|
|
1642
|
+
Usage:
|
|
1643
|
+
planflow_task_next(projectId: "uuid")
|
|
1644
|
+
|
|
1645
|
+
Parameters:
|
|
1646
|
+
- projectId (required): The project UUID (get from planflow_projects)
|
|
1647
|
+
|
|
1648
|
+
Returns:
|
|
1649
|
+
- Recommended task with details
|
|
1650
|
+
- Reasoning for the recommendation
|
|
1651
|
+
- Alternative tasks if the main recommendation doesn't fit
|
|
1652
|
+
- Project progress overview
|
|
1653
|
+
|
|
1654
|
+
You must be logged in first with planflow_login.`,
|
|
1655
|
+
inputSchema: TaskNextInputSchema,
|
|
1656
|
+
async execute(input) {
|
|
1657
|
+
logger.info("Finding next task recommendation", { projectId: input.projectId });
|
|
1658
|
+
if (!isAuthenticated()) {
|
|
1659
|
+
logger.debug("No active session found");
|
|
1660
|
+
return createErrorResult(
|
|
1661
|
+
'\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'
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
try {
|
|
1665
|
+
const client = getApiClient();
|
|
1666
|
+
const response = await client.listTasks(input.projectId);
|
|
1667
|
+
logger.info("Successfully retrieved tasks for analysis", {
|
|
1668
|
+
projectId: input.projectId,
|
|
1669
|
+
count: response.tasks.length
|
|
1670
|
+
});
|
|
1671
|
+
const tasks = response.tasks;
|
|
1672
|
+
if (tasks.length === 0) {
|
|
1673
|
+
return createSuccessResult(
|
|
1674
|
+
`\u{1F4CB} No tasks found in project "${response.projectName}".
|
|
1675
|
+
|
|
1676
|
+
\u{1F4A1} Tasks are created when you sync your PROJECT_PLAN.md:
|
|
1677
|
+
planflow_sync(projectId: "${input.projectId}", direction: "push")`
|
|
1678
|
+
);
|
|
1679
|
+
}
|
|
1680
|
+
const completedTaskIds = new Set(
|
|
1681
|
+
tasks.filter((t) => t.status === "DONE").map((t) => t.taskId)
|
|
1682
|
+
);
|
|
1683
|
+
const currentPhase = findCurrentPhase(tasks);
|
|
1684
|
+
const recentComplexity = getRecentComplexity(tasks);
|
|
1685
|
+
const availableTasks = tasks.filter(
|
|
1686
|
+
(task) => task.status === "TODO" && areDependenciesSatisfied(task.dependencies, completedTaskIds)
|
|
1687
|
+
);
|
|
1688
|
+
const inProgressTasks = tasks.filter((t) => t.status === "IN_PROGRESS");
|
|
1689
|
+
const blockedTasks = tasks.filter((t) => t.status === "BLOCKED");
|
|
1690
|
+
const stats = {
|
|
1691
|
+
total: tasks.length,
|
|
1692
|
+
done: tasks.filter((t) => t.status === "DONE").length,
|
|
1693
|
+
inProgress: inProgressTasks.length,
|
|
1694
|
+
blocked: blockedTasks.length,
|
|
1695
|
+
todo: tasks.filter((t) => t.status === "TODO").length
|
|
1696
|
+
};
|
|
1697
|
+
const progressPercent = Math.round(stats.done / stats.total * 100);
|
|
1698
|
+
const progressBarLength = 10;
|
|
1699
|
+
const filledBlocks = Math.floor(progressPercent / 10);
|
|
1700
|
+
const progressBar = "\u{1F7E9}".repeat(filledBlocks) + "\u2B1C".repeat(progressBarLength - filledBlocks);
|
|
1701
|
+
if (stats.done === stats.total) {
|
|
1702
|
+
return createSuccessResult(
|
|
1703
|
+
`\u{1F389} Congratulations! All tasks completed!
|
|
1704
|
+
|
|
1705
|
+
\u2705 Project: ${response.projectName}
|
|
1706
|
+
\u{1F4CA} Progress: ${progressBar} 100%
|
|
1707
|
+
\u{1F3C6} ${stats.total} tasks completed
|
|
1708
|
+
|
|
1709
|
+
Project Status: \u2705 COMPLETE
|
|
1710
|
+
|
|
1711
|
+
\u{1F3AF} What's next?
|
|
1712
|
+
\u2022 Deploy to production (if not already)
|
|
1713
|
+
\u2022 Write post-mortem / lessons learned
|
|
1714
|
+
\u2022 Gather user feedback
|
|
1715
|
+
\u2022 Plan next version/features
|
|
1716
|
+
\u2022 Celebrate your success! \u{1F38A}
|
|
1717
|
+
|
|
1718
|
+
Great work on completing this project! \u{1F680}`
|
|
1719
|
+
);
|
|
1720
|
+
}
|
|
1721
|
+
if (availableTasks.length === 0) {
|
|
1722
|
+
let output2 = `\u26A0\uFE0F No tasks currently available to start.
|
|
1723
|
+
|
|
1724
|
+
\u{1F4CA} Project Status:
|
|
1725
|
+
${progressBar} ${progressPercent}%
|
|
1726
|
+
\u2705 Completed: ${stats.done}/${stats.total}
|
|
1727
|
+
\u{1F504} In Progress: ${stats.inProgress}
|
|
1728
|
+
\u{1F6AB} Blocked: ${stats.blocked}
|
|
1729
|
+
\u23F3 Waiting on Dependencies: ${stats.todo - availableTasks.length}
|
|
1730
|
+
`;
|
|
1731
|
+
if (inProgressTasks.length > 0) {
|
|
1732
|
+
output2 += `
|
|
1733
|
+
\u{1F504} Tasks In Progress:
|
|
1734
|
+
`;
|
|
1735
|
+
for (const task of inProgressTasks) {
|
|
1736
|
+
output2 += ` \u2022 ${task.taskId}: ${task.name}
|
|
1737
|
+
`;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
if (blockedTasks.length > 0) {
|
|
1741
|
+
output2 += `
|
|
1742
|
+
\u{1F6AB} Blocked Tasks:
|
|
1743
|
+
`;
|
|
1744
|
+
for (const task of blockedTasks) {
|
|
1745
|
+
output2 += ` \u2022 ${task.taskId}: ${task.name}
|
|
1746
|
+
`;
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
output2 += `
|
|
1750
|
+
\u{1F4A1} Suggested Actions:
|
|
1751
|
+
1. Complete in-progress tasks
|
|
1752
|
+
2. Resolve blockers on blocked tasks
|
|
1753
|
+
3. Review dependencies if tasks seem stuck
|
|
1754
|
+
|
|
1755
|
+
\u{1F4A1} Commands:
|
|
1756
|
+
\u2022 planflow_task_list(projectId: "${input.projectId}", status: "IN_PROGRESS")
|
|
1757
|
+
\u2022 planflow_task_update(projectId: "${input.projectId}", taskId: "TX.Y", status: "DONE")`;
|
|
1758
|
+
return createSuccessResult(output2);
|
|
1759
|
+
}
|
|
1760
|
+
const scoredTasks = availableTasks.map(
|
|
1761
|
+
(task) => scoreTask(task, currentPhase, completedTaskIds, tasks, recentComplexity)
|
|
1762
|
+
).sort((a, b) => b.score - a.score);
|
|
1763
|
+
const recommended = scoredTasks[0];
|
|
1764
|
+
const alternatives = scoredTasks.slice(1, 4);
|
|
1765
|
+
let inProgressWarning = "";
|
|
1766
|
+
if (inProgressTasks.length >= 3) {
|
|
1767
|
+
inProgressWarning = `\u26A0\uFE0F You have ${inProgressTasks.length} tasks in progress.
|
|
1768
|
+
|
|
1769
|
+
\u{1F4A1} Tip: Consider finishing in-progress tasks before starting new ones:
|
|
1770
|
+
`;
|
|
1771
|
+
for (const task of inProgressTasks.slice(0, 3)) {
|
|
1772
|
+
inProgressWarning += ` \u2022 ${task.taskId}: ${task.name}
|
|
1773
|
+
`;
|
|
1774
|
+
}
|
|
1775
|
+
inProgressWarning += `
|
|
1776
|
+
Benefits of finishing first:
|
|
1777
|
+
\u2022 Clear sense of progress
|
|
1778
|
+
\u2022 Unlock dependent tasks
|
|
1779
|
+
\u2022 Maintain focus and momentum
|
|
1780
|
+
|
|
1781
|
+
${"\u2500".repeat(60)}
|
|
1782
|
+
|
|
1783
|
+
Still want to start something new? Here's the recommendation:
|
|
1784
|
+
|
|
1785
|
+
`;
|
|
1786
|
+
}
|
|
1787
|
+
const complexityIndicator = getComplexityIndicator3(recommended.complexity);
|
|
1788
|
+
let output = inProgressWarning + `\u{1F3AF} Recommended Next Task
|
|
1789
|
+
|
|
1790
|
+
${recommended.taskId}: ${recommended.name}
|
|
1791
|
+
|
|
1792
|
+
` + formatKeyValue({
|
|
1793
|
+
"Complexity": `${complexityIndicator} ${recommended.complexity}`,
|
|
1794
|
+
"Estimated": recommended.estimatedHours ? `${recommended.estimatedHours} hours` : "Not estimated",
|
|
1795
|
+
"Phase": `${recommended.phase}`,
|
|
1796
|
+
"Dependencies": recommended.dependencies.length > 0 ? `${recommended.dependencies.join(", ")} \u2705` : "None"
|
|
1797
|
+
}) + `
|
|
1798
|
+
|
|
1799
|
+
\u2705 All dependencies completed
|
|
1800
|
+
`;
|
|
1801
|
+
if (recommended.reasons.length > 0) {
|
|
1802
|
+
output += `
|
|
1803
|
+
\u{1F3AF} Why this task?
|
|
1804
|
+
`;
|
|
1805
|
+
for (const reason of recommended.reasons) {
|
|
1806
|
+
output += ` \u2022 ${reason}
|
|
1807
|
+
`;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
if (recommended.description) {
|
|
1811
|
+
output += `
|
|
1812
|
+
\u{1F4DD} Task Details:
|
|
1813
|
+
${recommended.description}
|
|
1814
|
+
`;
|
|
1815
|
+
}
|
|
1816
|
+
output += `
|
|
1817
|
+
${"\u2500".repeat(60)}
|
|
1818
|
+
|
|
1819
|
+
Ready to start?
|
|
1820
|
+
planflow_task_update(projectId: "${input.projectId}", taskId: "${recommended.taskId}", status: "IN_PROGRESS")
|
|
1821
|
+
`;
|
|
1822
|
+
if (alternatives.length > 0) {
|
|
1823
|
+
output += `
|
|
1824
|
+
${"\u2500".repeat(60)}
|
|
1825
|
+
|
|
1826
|
+
`;
|
|
1827
|
+
output += `\u{1F4A1} Alternative Tasks (if this doesn't fit):
|
|
1828
|
+
|
|
1829
|
+
`;
|
|
1830
|
+
for (let i = 0; i < alternatives.length; i++) {
|
|
1831
|
+
const alt = alternatives[i];
|
|
1832
|
+
const altComplexity = getComplexityIndicator3(alt.complexity);
|
|
1833
|
+
output += `${i + 1}. ${alt.taskId}: ${alt.name}
|
|
1834
|
+
`;
|
|
1835
|
+
output += ` ${altComplexity} ${alt.complexity}`;
|
|
1836
|
+
if (alt.estimatedHours) {
|
|
1837
|
+
output += ` \u2022 ${alt.estimatedHours}h`;
|
|
1838
|
+
}
|
|
1839
|
+
if (alt.unlocksCount > 0) {
|
|
1840
|
+
output += ` \u2022 Unlocks ${alt.unlocksCount} task${alt.unlocksCount > 1 ? "s" : ""}`;
|
|
1841
|
+
}
|
|
1842
|
+
output += "\n";
|
|
1843
|
+
}
|
|
1844
|
+
}
|
|
1845
|
+
output += `
|
|
1846
|
+
${"\u2500".repeat(60)}
|
|
1847
|
+
|
|
1848
|
+
`;
|
|
1849
|
+
output += `\u{1F4CA} Progress: ${progressBar} ${progressPercent}%
|
|
1850
|
+
`;
|
|
1851
|
+
output += `Total: ${stats.total} | \u2705 ${stats.done} | \u{1F504} ${stats.inProgress} | \u{1F4CB} ${stats.todo} | \u{1F6AB} ${stats.blocked}`;
|
|
1852
|
+
if (progressPercent < 25) {
|
|
1853
|
+
output += `
|
|
1854
|
+
|
|
1855
|
+
\u{1F31F} Early Stage Tips:
|
|
1856
|
+
\u2022 Focus on foundation tasks
|
|
1857
|
+
\u2022 Don't skip setup steps
|
|
1858
|
+
\u2022 Document as you go`;
|
|
1859
|
+
} else if (progressPercent >= 75) {
|
|
1860
|
+
output += `
|
|
1861
|
+
|
|
1862
|
+
\u{1F3C1} Final Sprint:
|
|
1863
|
+
\u2022 Almost there!
|
|
1864
|
+
\u2022 Don't rush quality
|
|
1865
|
+
\u2022 Test thoroughly`;
|
|
1866
|
+
}
|
|
1867
|
+
logger.info("Successfully generated task recommendation", {
|
|
1868
|
+
recommendedTask: recommended.taskId,
|
|
1869
|
+
score: recommended.score,
|
|
1870
|
+
alternativeCount: alternatives.length
|
|
1871
|
+
});
|
|
1872
|
+
return createSuccessResult(output);
|
|
1873
|
+
} catch (error) {
|
|
1874
|
+
logger.error("Failed to find next task", {
|
|
1875
|
+
error: String(error),
|
|
1876
|
+
projectId: input.projectId
|
|
1877
|
+
});
|
|
1878
|
+
if (error instanceof AuthError) {
|
|
1879
|
+
return createErrorResult(
|
|
1880
|
+
'\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'
|
|
1881
|
+
);
|
|
1882
|
+
}
|
|
1883
|
+
if (error instanceof ApiError) {
|
|
1884
|
+
if (error.statusCode === 404) {
|
|
1885
|
+
return createErrorResult(
|
|
1886
|
+
`\u274C Project not found: ${input.projectId}
|
|
1887
|
+
|
|
1888
|
+
Please check the project ID and try again.
|
|
1889
|
+
Use planflow_projects() to list your available projects.`
|
|
1890
|
+
);
|
|
1891
|
+
}
|
|
1892
|
+
return createErrorResult(
|
|
1893
|
+
`\u274C API error: ${error.message}
|
|
1894
|
+
|
|
1895
|
+
Please check your internet connection and try again.`
|
|
1896
|
+
);
|
|
1897
|
+
}
|
|
1898
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1899
|
+
return createErrorResult(
|
|
1900
|
+
`\u274C Failed to find next task: ${message}
|
|
1901
|
+
|
|
1902
|
+
Please try again or check your connection.`
|
|
1903
|
+
);
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
};
|
|
1907
|
+
|
|
1908
|
+
// src/tools/notifications.ts
|
|
1909
|
+
import { z as z11 } from "zod";
|
|
1910
|
+
var NotificationsInputSchema = z11.object({
|
|
1911
|
+
action: z11.enum(["list", "read", "read-all"]).default("list").describe("Action to perform: list, read (mark one as read), or read-all"),
|
|
1912
|
+
projectId: z11.string().uuid("Project ID must be a valid UUID").optional().describe("Filter notifications by project (optional)"),
|
|
1913
|
+
notificationId: z11.string().uuid("Notification ID must be a valid UUID").optional().describe('Notification ID to mark as read (required for "read" action)'),
|
|
1914
|
+
unreadOnly: z11.boolean().default(true).describe("Only show unread notifications (default: true)"),
|
|
1915
|
+
limit: z11.number().int().min(1).max(100).default(20).describe("Maximum number of notifications to fetch (default: 20)")
|
|
1916
|
+
});
|
|
1917
|
+
function getTypeEmoji(type) {
|
|
1918
|
+
switch (type) {
|
|
1919
|
+
case "comment":
|
|
1920
|
+
return "\u{1F4AC}";
|
|
1921
|
+
case "status_change":
|
|
1922
|
+
return "\u{1F504}";
|
|
1923
|
+
case "task_assigned":
|
|
1924
|
+
return "\u{1F464}";
|
|
1925
|
+
case "task_blocked":
|
|
1926
|
+
return "\u{1F6AB}";
|
|
1927
|
+
case "task_unblocked":
|
|
1928
|
+
return "\u2705";
|
|
1929
|
+
case "mention":
|
|
1930
|
+
return "\u{1F4E3}";
|
|
1931
|
+
default:
|
|
1932
|
+
return "\u{1F514}";
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
function formatRelativeTime(dateString) {
|
|
1936
|
+
const date = new Date(dateString);
|
|
1937
|
+
const now = /* @__PURE__ */ new Date();
|
|
1938
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1939
|
+
const diffMinutes = Math.floor(diffMs / 6e4);
|
|
1940
|
+
const diffHours = Math.floor(diffMinutes / 60);
|
|
1941
|
+
const diffDays = Math.floor(diffHours / 24);
|
|
1942
|
+
if (diffMinutes < 1) return "just now";
|
|
1943
|
+
if (diffMinutes < 60) return `${diffMinutes} min ago`;
|
|
1944
|
+
if (diffHours < 24) return `${diffHours}h ago`;
|
|
1945
|
+
if (diffDays < 7) return `${diffDays}d ago`;
|
|
1946
|
+
return date.toLocaleDateString();
|
|
1947
|
+
}
|
|
1948
|
+
function truncate3(str, maxLength) {
|
|
1949
|
+
if (!str) return "-";
|
|
1950
|
+
if (str.length <= maxLength) return str;
|
|
1951
|
+
return str.slice(0, maxLength - 3) + "...";
|
|
1952
|
+
}
|
|
1953
|
+
var notificationsTool = {
|
|
1954
|
+
name: "planflow_notifications",
|
|
1955
|
+
description: `View and manage PlanFlow notifications.
|
|
1956
|
+
|
|
1957
|
+
Get notified about task updates, comments, and team activities.
|
|
1958
|
+
|
|
1959
|
+
Usage:
|
|
1960
|
+
planflow_notifications() # List unread notifications
|
|
1961
|
+
planflow_notifications(unreadOnly: false) # List all notifications
|
|
1962
|
+
planflow_notifications(projectId: "uuid") # Filter by project
|
|
1963
|
+
planflow_notifications(action: "read", notificationId: "uuid") # Mark as read
|
|
1964
|
+
planflow_notifications(action: "read-all") # Mark all as read
|
|
1965
|
+
|
|
1966
|
+
Parameters:
|
|
1967
|
+
- action (optional): "list" (default), "read", or "read-all"
|
|
1968
|
+
- projectId (optional): Filter by project UUID
|
|
1969
|
+
- notificationId (required for "read"): Notification UUID to mark as read
|
|
1970
|
+
- unreadOnly (optional): Only show unread (default: true)
|
|
1971
|
+
- limit (optional): Max notifications to fetch (default: 20, max: 100)
|
|
1972
|
+
|
|
1973
|
+
Notification Types:
|
|
1974
|
+
- comment: Someone commented on a task
|
|
1975
|
+
- status_change: Task status was updated
|
|
1976
|
+
- task_assigned: You were assigned to a task
|
|
1977
|
+
- task_blocked: A task was blocked
|
|
1978
|
+
- task_unblocked: A task was unblocked
|
|
1979
|
+
- mention: You were mentioned in a comment
|
|
1980
|
+
|
|
1981
|
+
You must be logged in first with planflow_login.`,
|
|
1982
|
+
inputSchema: NotificationsInputSchema,
|
|
1983
|
+
async execute(input) {
|
|
1984
|
+
logger.info("Notifications tool called", { action: input.action, projectId: input.projectId });
|
|
1985
|
+
if (!isAuthenticated()) {
|
|
1986
|
+
logger.debug("No active session found");
|
|
1987
|
+
return createErrorResult(
|
|
1988
|
+
'\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'
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
try {
|
|
1992
|
+
const client = getApiClient();
|
|
1993
|
+
switch (input.action) {
|
|
1994
|
+
case "read": {
|
|
1995
|
+
if (!input.notificationId) {
|
|
1996
|
+
return createErrorResult(
|
|
1997
|
+
'\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.'
|
|
1998
|
+
);
|
|
1999
|
+
}
|
|
2000
|
+
logger.info("Marking notification as read", { notificationId: input.notificationId });
|
|
2001
|
+
const result = await client.markNotificationRead(input.notificationId);
|
|
2002
|
+
return createSuccessResult(
|
|
2003
|
+
`\u2705 Notification marked as read
|
|
2004
|
+
|
|
2005
|
+
${getTypeEmoji(result.notification.type)} ${result.notification.message}
|
|
2006
|
+
|
|
2007
|
+
\u{1F4A1} Commands:
|
|
2008
|
+
\u2022 planflow_notifications() - View remaining notifications`
|
|
2009
|
+
);
|
|
2010
|
+
}
|
|
2011
|
+
case "read-all": {
|
|
2012
|
+
logger.info("Marking all notifications as read", { projectId: input.projectId });
|
|
2013
|
+
const result = await client.markAllNotificationsRead(input.projectId);
|
|
2014
|
+
const scopeMessage = input.projectId ? "for this project" : "across all projects";
|
|
2015
|
+
return createSuccessResult(
|
|
2016
|
+
`\u2705 Marked ${result.markedCount} notification${result.markedCount !== 1 ? "s" : ""} as read ${scopeMessage}
|
|
2017
|
+
|
|
2018
|
+
\u{1F514} You're all caught up!
|
|
2019
|
+
|
|
2020
|
+
\u{1F4A1} Commands:
|
|
2021
|
+
\u2022 planflow_notifications(unreadOnly: false) - View all notifications`
|
|
2022
|
+
);
|
|
2023
|
+
}
|
|
2024
|
+
case "list":
|
|
2025
|
+
default: {
|
|
2026
|
+
logger.info("Fetching notifications", {
|
|
2027
|
+
projectId: input.projectId,
|
|
2028
|
+
unreadOnly: input.unreadOnly,
|
|
2029
|
+
limit: input.limit
|
|
2030
|
+
});
|
|
2031
|
+
const response = await client.listNotifications({
|
|
2032
|
+
projectId: input.projectId,
|
|
2033
|
+
unreadOnly: input.unreadOnly,
|
|
2034
|
+
limit: input.limit
|
|
2035
|
+
});
|
|
2036
|
+
logger.info("Successfully retrieved notifications", {
|
|
2037
|
+
count: response.notifications.length,
|
|
2038
|
+
unreadCount: response.unreadCount,
|
|
2039
|
+
totalCount: response.totalCount
|
|
2040
|
+
});
|
|
2041
|
+
if (response.notifications.length === 0) {
|
|
2042
|
+
const filterMessage = input.unreadOnly ? "unread " : "";
|
|
2043
|
+
const projectMessage = input.projectId ? " for this project" : "";
|
|
2044
|
+
return createSuccessResult(
|
|
2045
|
+
`\u{1F514} No ${filterMessage}notifications${projectMessage}
|
|
2046
|
+
|
|
2047
|
+
` + (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")
|
|
2048
|
+
);
|
|
2049
|
+
}
|
|
2050
|
+
const headers = ["Type", "Message", "Project", "Task", "Time", "Read"];
|
|
2051
|
+
const rows = response.notifications.map((n) => [
|
|
2052
|
+
getTypeEmoji(n.type),
|
|
2053
|
+
truncate3(n.message, 35),
|
|
2054
|
+
truncate3(n.projectName, 15),
|
|
2055
|
+
n.taskId ?? "-",
|
|
2056
|
+
formatRelativeTime(n.createdAt),
|
|
2057
|
+
n.read ? "\u2713" : "\u2022"
|
|
2058
|
+
]);
|
|
2059
|
+
const filterLabel = input.unreadOnly ? " (unread only)" : "";
|
|
2060
|
+
const projectLabel = input.projectId ? ` for project` : "";
|
|
2061
|
+
const output = [
|
|
2062
|
+
`\u{1F514} Notifications${filterLabel}${projectLabel}
|
|
2063
|
+
`,
|
|
2064
|
+
`Unread: ${response.unreadCount} | Total: ${response.totalCount}
|
|
2065
|
+
`,
|
|
2066
|
+
formatTable(headers, rows),
|
|
2067
|
+
"\n\n\u{1F4A1} Commands:",
|
|
2068
|
+
' \u2022 planflow_notifications(action: "read", notificationId: "uuid") - Mark as read',
|
|
2069
|
+
' \u2022 planflow_notifications(action: "read-all") - Mark all as read',
|
|
2070
|
+
input.unreadOnly ? " \u2022 planflow_notifications(unreadOnly: false) - Show all notifications" : " \u2022 planflow_notifications() - Show only unread"
|
|
2071
|
+
].join("\n");
|
|
2072
|
+
return createSuccessResult(output);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
} catch (error) {
|
|
2076
|
+
logger.error("Failed to handle notifications", { error: String(error), action: input.action });
|
|
2077
|
+
if (error instanceof AuthError) {
|
|
2078
|
+
return createErrorResult(
|
|
2079
|
+
'\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'
|
|
2080
|
+
);
|
|
2081
|
+
}
|
|
2082
|
+
if (error instanceof ApiError) {
|
|
2083
|
+
if (error.statusCode === 404) {
|
|
2084
|
+
if (input.action === "read") {
|
|
2085
|
+
return createErrorResult(
|
|
2086
|
+
`\u274C Notification not found: ${input.notificationId}
|
|
2087
|
+
|
|
2088
|
+
The notification may have been deleted or the ID is incorrect.
|
|
2089
|
+
Use planflow_notifications() to list available notifications.`
|
|
2090
|
+
);
|
|
2091
|
+
}
|
|
2092
|
+
return createErrorResult(
|
|
2093
|
+
`\u274C Resource not found
|
|
2094
|
+
|
|
2095
|
+
Please check the project ID and try again.
|
|
2096
|
+
Use planflow_projects() to list your available projects.`
|
|
2097
|
+
);
|
|
2098
|
+
}
|
|
2099
|
+
return createErrorResult(
|
|
2100
|
+
`\u274C API error: ${error.message}
|
|
2101
|
+
|
|
2102
|
+
Please check your internet connection and try again.`
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2106
|
+
return createErrorResult(
|
|
2107
|
+
`\u274C Failed to handle notifications: ${message}
|
|
2108
|
+
|
|
2109
|
+
Please try again or check your connection.`
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
}
|
|
2113
|
+
};
|
|
2114
|
+
|
|
2115
|
+
// src/tools/index.ts
|
|
2116
|
+
var tools = [
|
|
2117
|
+
loginTool,
|
|
2118
|
+
logoutTool,
|
|
2119
|
+
whoamiTool,
|
|
2120
|
+
projectsTool,
|
|
2121
|
+
createTool,
|
|
2122
|
+
syncTool,
|
|
2123
|
+
taskListTool,
|
|
2124
|
+
taskUpdateTool,
|
|
2125
|
+
taskNextTool,
|
|
2126
|
+
notificationsTool
|
|
2127
|
+
];
|
|
2128
|
+
|
|
2129
|
+
// src/server.ts
|
|
2130
|
+
function formatErrorResponse(error) {
|
|
2131
|
+
let message;
|
|
2132
|
+
if (error instanceof PlanFlowError) {
|
|
2133
|
+
message = `Error [${error.code}]: ${error.message}`;
|
|
2134
|
+
if (error.details) {
|
|
2135
|
+
message += `
|
|
2136
|
+
Details: ${JSON.stringify(error.details, null, 2)}`;
|
|
2137
|
+
}
|
|
2138
|
+
} else if (error instanceof Error) {
|
|
2139
|
+
message = `Error: ${error.message}`;
|
|
2140
|
+
} else {
|
|
2141
|
+
message = `Error: ${String(error)}`;
|
|
2142
|
+
}
|
|
2143
|
+
return {
|
|
2144
|
+
content: [{ type: "text", text: message }],
|
|
2145
|
+
isError: true
|
|
2146
|
+
};
|
|
2147
|
+
}
|
|
2148
|
+
function zodToJsonSchema(schema) {
|
|
2149
|
+
if ("shape" in schema && schema.shape) {
|
|
2150
|
+
const shape = schema.shape;
|
|
2151
|
+
const properties = {};
|
|
2152
|
+
const required = [];
|
|
2153
|
+
for (const [key, value] of Object.entries(shape)) {
|
|
2154
|
+
const zodField = value;
|
|
2155
|
+
let type = "string";
|
|
2156
|
+
const typeName = zodField._def?.typeName;
|
|
2157
|
+
if (typeName === "ZodNumber") type = "number";
|
|
2158
|
+
if (typeName === "ZodBoolean") type = "boolean";
|
|
2159
|
+
if (typeName === "ZodArray") type = "array";
|
|
2160
|
+
properties[key] = {
|
|
2161
|
+
type,
|
|
2162
|
+
description: zodField._def?.description
|
|
2163
|
+
};
|
|
2164
|
+
if (typeof zodField.isOptional !== "function" || !zodField.isOptional()) {
|
|
2165
|
+
required.push(key);
|
|
2166
|
+
}
|
|
2167
|
+
}
|
|
2168
|
+
return {
|
|
2169
|
+
type: "object",
|
|
2170
|
+
properties,
|
|
2171
|
+
required: required.length > 0 ? required : void 0
|
|
2172
|
+
};
|
|
2173
|
+
}
|
|
2174
|
+
return { type: "object", properties: {} };
|
|
2175
|
+
}
|
|
2176
|
+
function createServer() {
|
|
2177
|
+
const server = new Server(
|
|
2178
|
+
{
|
|
2179
|
+
name: APP_NAME,
|
|
2180
|
+
version: APP_VERSION
|
|
2181
|
+
},
|
|
2182
|
+
{
|
|
2183
|
+
capabilities: {
|
|
2184
|
+
tools: {}
|
|
2185
|
+
}
|
|
2186
|
+
}
|
|
2187
|
+
);
|
|
2188
|
+
server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2189
|
+
logger.debug("Listing tools", { count: tools.length });
|
|
2190
|
+
return {
|
|
2191
|
+
tools: tools.map((tool) => ({
|
|
2192
|
+
name: tool.name,
|
|
2193
|
+
description: tool.description,
|
|
2194
|
+
inputSchema: zodToJsonSchema(tool.inputSchema)
|
|
2195
|
+
}))
|
|
2196
|
+
};
|
|
2197
|
+
});
|
|
2198
|
+
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
2199
|
+
const { name, arguments: args } = request.params;
|
|
2200
|
+
logger.info("Tool called", { name });
|
|
2201
|
+
const tool = tools.find((t) => t.name === name);
|
|
2202
|
+
if (!tool) {
|
|
2203
|
+
logger.warn("Unknown tool requested", { name });
|
|
2204
|
+
return formatErrorResponse(new ToolError(`Unknown tool: ${name}`, name));
|
|
2205
|
+
}
|
|
2206
|
+
try {
|
|
2207
|
+
const parseResult = tool.inputSchema.safeParse(args);
|
|
2208
|
+
if (!parseResult.success) {
|
|
2209
|
+
logger.warn("Tool input validation failed", {
|
|
2210
|
+
name,
|
|
2211
|
+
errors: parseResult.error.errors
|
|
2212
|
+
});
|
|
2213
|
+
return formatErrorResponse(
|
|
2214
|
+
new PlanFlowError(
|
|
2215
|
+
`Invalid input: ${parseResult.error.errors.map((e) => e.message).join(", ")}`,
|
|
2216
|
+
"VALIDATION_ERROR",
|
|
2217
|
+
{ errors: parseResult.error.errors }
|
|
2218
|
+
)
|
|
2219
|
+
);
|
|
2220
|
+
}
|
|
2221
|
+
const result = await tool.execute(parseResult.data);
|
|
2222
|
+
logger.debug("Tool executed successfully", { name });
|
|
2223
|
+
return result;
|
|
2224
|
+
} catch (error) {
|
|
2225
|
+
logger.error("Tool execution failed", {
|
|
2226
|
+
name,
|
|
2227
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2228
|
+
});
|
|
2229
|
+
return formatErrorResponse(error);
|
|
2230
|
+
}
|
|
2231
|
+
});
|
|
2232
|
+
return server;
|
|
2233
|
+
}
|
|
2234
|
+
async function startServer() {
|
|
2235
|
+
logger.info("Starting PlanFlow MCP Server", { version: APP_VERSION });
|
|
2236
|
+
const server = createServer();
|
|
2237
|
+
const transport = new StdioServerTransport();
|
|
2238
|
+
server.onerror = (error) => {
|
|
2239
|
+
logger.error("Server error", { error: String(error) });
|
|
2240
|
+
};
|
|
2241
|
+
const shutdown = async () => {
|
|
2242
|
+
logger.info("Shutting down server...");
|
|
2243
|
+
await server.close();
|
|
2244
|
+
process.exit(0);
|
|
2245
|
+
};
|
|
2246
|
+
process.on("SIGINT", shutdown);
|
|
2247
|
+
process.on("SIGTERM", shutdown);
|
|
2248
|
+
await server.connect(transport);
|
|
2249
|
+
logger.info("Server connected and ready");
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
// src/index.ts
|
|
2253
|
+
if (process.env["PLANFLOW_DEBUG"] === "true") {
|
|
2254
|
+
logger.setLevel("debug");
|
|
2255
|
+
}
|
|
2256
|
+
startServer().catch((error) => {
|
|
2257
|
+
logger.error("Failed to start server", {
|
|
2258
|
+
error: error instanceof Error ? error.message : String(error)
|
|
2259
|
+
});
|
|
2260
|
+
process.exit(1);
|
|
2261
|
+
});
|