@shin1ohno/sage 0.7.9 → 0.8.6

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.
Files changed (149) hide show
  1. package/README.md +26 -8
  2. package/dist/cli/mcp-handler.d.ts.map +1 -1
  3. package/dist/cli/mcp-handler.js +141 -987
  4. package/dist/cli/mcp-handler.js.map +1 -1
  5. package/dist/config/loader.d.ts.map +1 -1
  6. package/dist/config/loader.js +30 -1
  7. package/dist/config/loader.js.map +1 -1
  8. package/dist/config/update-validation.d.ts +52 -0
  9. package/dist/config/update-validation.d.ts.map +1 -0
  10. package/dist/config/update-validation.js +133 -0
  11. package/dist/config/update-validation.js.map +1 -0
  12. package/dist/config/validation.d.ts +130 -0
  13. package/dist/config/validation.d.ts.map +1 -0
  14. package/dist/config/validation.js +53 -0
  15. package/dist/config/validation.js.map +1 -0
  16. package/dist/index.js +443 -1632
  17. package/dist/index.js.map +1 -1
  18. package/dist/integrations/calendar-event-creator.d.ts +2 -3
  19. package/dist/integrations/calendar-event-creator.d.ts.map +1 -1
  20. package/dist/integrations/calendar-event-creator.js +3 -4
  21. package/dist/integrations/calendar-event-creator.js.map +1 -1
  22. package/dist/integrations/calendar-event-deleter.d.ts +2 -3
  23. package/dist/integrations/calendar-event-deleter.d.ts.map +1 -1
  24. package/dist/integrations/calendar-event-deleter.js +3 -4
  25. package/dist/integrations/calendar-event-deleter.js.map +1 -1
  26. package/dist/integrations/calendar-event-response.d.ts +4 -17
  27. package/dist/integrations/calendar-event-response.d.ts.map +1 -1
  28. package/dist/integrations/calendar-event-response.js +3 -4
  29. package/dist/integrations/calendar-event-response.js.map +1 -1
  30. package/dist/integrations/calendar-service.d.ts +6 -3
  31. package/dist/integrations/calendar-service.d.ts.map +1 -1
  32. package/dist/integrations/calendar-service.js +26 -4
  33. package/dist/integrations/calendar-service.js.map +1 -1
  34. package/dist/integrations/calendar-source-manager.d.ts +302 -0
  35. package/dist/integrations/calendar-source-manager.d.ts.map +1 -0
  36. package/dist/integrations/calendar-source-manager.js +862 -0
  37. package/dist/integrations/calendar-source-manager.js.map +1 -0
  38. package/dist/integrations/google-calendar-service.d.ts +176 -0
  39. package/dist/integrations/google-calendar-service.d.ts.map +1 -0
  40. package/dist/integrations/google-calendar-service.js +745 -0
  41. package/dist/integrations/google-calendar-service.js.map +1 -0
  42. package/dist/integrations/notion-mcp.d.ts +28 -3
  43. package/dist/integrations/notion-mcp.d.ts.map +1 -1
  44. package/dist/integrations/notion-mcp.js +21 -5
  45. package/dist/integrations/notion-mcp.js.map +1 -1
  46. package/dist/integrations/reminder-manager.d.ts.map +1 -1
  47. package/dist/integrations/reminder-manager.js +2 -0
  48. package/dist/integrations/reminder-manager.js.map +1 -1
  49. package/dist/oauth/google-oauth-handler.d.ts +149 -0
  50. package/dist/oauth/google-oauth-handler.d.ts.map +1 -0
  51. package/dist/oauth/google-oauth-handler.js +365 -0
  52. package/dist/oauth/google-oauth-handler.js.map +1 -0
  53. package/dist/services/container.d.ts +56 -0
  54. package/dist/services/container.d.ts.map +1 -0
  55. package/dist/services/container.js +76 -0
  56. package/dist/services/container.js.map +1 -0
  57. package/dist/tools/calendar/handlers.d.ts +186 -0
  58. package/dist/tools/calendar/handlers.d.ts.map +1 -0
  59. package/dist/tools/calendar/handlers.js +525 -0
  60. package/dist/tools/calendar/handlers.js.map +1 -0
  61. package/dist/tools/calendar/index.d.ts +11 -0
  62. package/dist/tools/calendar/index.d.ts.map +1 -0
  63. package/dist/tools/calendar/index.js +10 -0
  64. package/dist/tools/calendar/index.js.map +1 -0
  65. package/dist/tools/index.d.ts +23 -0
  66. package/dist/tools/index.d.ts.map +1 -0
  67. package/dist/tools/index.js +24 -0
  68. package/dist/tools/index.js.map +1 -0
  69. package/dist/tools/integrations/handlers.d.ts +57 -0
  70. package/dist/tools/integrations/handlers.d.ts.map +1 -0
  71. package/dist/tools/integrations/handlers.js +159 -0
  72. package/dist/tools/integrations/handlers.js.map +1 -0
  73. package/dist/tools/integrations/index.d.ts +11 -0
  74. package/dist/tools/integrations/index.d.ts.map +1 -0
  75. package/dist/tools/integrations/index.js +10 -0
  76. package/dist/tools/integrations/index.js.map +1 -0
  77. package/dist/tools/registry.d.ts +8 -0
  78. package/dist/tools/registry.d.ts.map +1 -0
  79. package/dist/tools/registry.js +10 -0
  80. package/dist/tools/registry.js.map +1 -0
  81. package/dist/tools/reminders/handlers.d.ts +61 -0
  82. package/dist/tools/reminders/handlers.d.ts.map +1 -0
  83. package/dist/tools/reminders/handlers.js +148 -0
  84. package/dist/tools/reminders/handlers.js.map +1 -0
  85. package/dist/tools/reminders/index.d.ts +11 -0
  86. package/dist/tools/reminders/index.d.ts.map +1 -0
  87. package/dist/tools/reminders/index.js +10 -0
  88. package/dist/tools/reminders/index.js.map +1 -0
  89. package/dist/tools/setup/handlers.d.ts +81 -0
  90. package/dist/tools/setup/handlers.d.ts.map +1 -0
  91. package/dist/tools/setup/handlers.js +172 -0
  92. package/dist/tools/setup/handlers.js.map +1 -0
  93. package/dist/tools/setup/index.d.ts +11 -0
  94. package/dist/tools/setup/index.d.ts.map +1 -0
  95. package/dist/tools/setup/index.js +10 -0
  96. package/dist/tools/setup/index.js.map +1 -0
  97. package/dist/tools/tasks/handlers.d.ts +95 -0
  98. package/dist/tools/tasks/handlers.d.ts.map +1 -0
  99. package/dist/tools/tasks/handlers.js +197 -0
  100. package/dist/tools/tasks/handlers.js.map +1 -0
  101. package/dist/tools/tasks/index.d.ts +11 -0
  102. package/dist/tools/tasks/index.d.ts.map +1 -0
  103. package/dist/tools/tasks/index.js +10 -0
  104. package/dist/tools/tasks/index.js.map +1 -0
  105. package/dist/tools/types.d.ts +54 -0
  106. package/dist/tools/types.d.ts.map +1 -0
  107. package/dist/tools/types.js +9 -0
  108. package/dist/tools/types.js.map +1 -0
  109. package/dist/types/calendar.d.ts +41 -0
  110. package/dist/types/calendar.d.ts.map +1 -0
  111. package/dist/types/calendar.js +18 -0
  112. package/dist/types/calendar.js.map +1 -0
  113. package/dist/types/config.d.ts +15 -0
  114. package/dist/types/config.d.ts.map +1 -1
  115. package/dist/types/config.js +21 -0
  116. package/dist/types/config.js.map +1 -1
  117. package/dist/types/google-calendar-types.d.ts +139 -0
  118. package/dist/types/google-calendar-types.d.ts.map +1 -0
  119. package/dist/types/google-calendar-types.js +46 -0
  120. package/dist/types/google-calendar-types.js.map +1 -0
  121. package/dist/types/index.d.ts +1 -0
  122. package/dist/types/index.d.ts.map +1 -1
  123. package/dist/types/index.js +1 -0
  124. package/dist/types/index.js.map +1 -1
  125. package/dist/utils/estimation.d.ts +34 -0
  126. package/dist/utils/estimation.d.ts.map +1 -1
  127. package/dist/utils/estimation.js +38 -1
  128. package/dist/utils/estimation.js.map +1 -1
  129. package/dist/utils/mcp-response.d.ts +89 -0
  130. package/dist/utils/mcp-response.d.ts.map +1 -0
  131. package/dist/utils/mcp-response.js +103 -0
  132. package/dist/utils/mcp-response.js.map +1 -0
  133. package/dist/utils/task-splitter.d.ts +65 -4
  134. package/dist/utils/task-splitter.d.ts.map +1 -1
  135. package/dist/utils/task-splitter.js +69 -5
  136. package/dist/utils/task-splitter.js.map +1 -1
  137. package/dist/version.d.ts +2 -2
  138. package/dist/version.d.ts.map +1 -1
  139. package/dist/version.js +22 -4
  140. package/dist/version.js.map +1 -1
  141. package/package.json +4 -3
  142. package/dist/cli/http-server.d.ts +0 -74
  143. package/dist/cli/http-server.d.ts.map +0 -1
  144. package/dist/cli/http-server.js +0 -407
  145. package/dist/cli/http-server.js.map +0 -1
  146. package/dist/remote/remote-mcp-server.d.ts +0 -244
  147. package/dist/remote/remote-mcp-server.d.ts.map +0 -1
  148. package/dist/remote/remote-mcp-server.js +0 -507
  149. package/dist/remote/remote-mcp-server.js.map +0 -1
package/dist/index.js CHANGED
@@ -9,135 +9,35 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
10
  import { z } from "zod";
11
11
  import { ConfigLoader } from "./config/loader.js";
12
- import { SetupWizard } from "./setup/wizard.js";
13
- import { TaskAnalyzer } from "./tools/analyze-tasks.js";
14
12
  import { ReminderManager } from "./integrations/reminder-manager.js";
15
13
  import { CalendarService } from "./integrations/calendar-service.js";
14
+ import { CalendarSourceManager } from "./integrations/calendar-source-manager.js";
15
+ import { GoogleCalendarService } from "./integrations/google-calendar-service.js";
16
16
  import { NotionMCPService } from "./integrations/notion-mcp.js";
17
17
  import { TodoListManager } from "./integrations/todo-list-manager.js";
18
18
  import { TaskSynchronizer } from "./integrations/task-synchronizer.js";
19
19
  import { CalendarEventResponseService } from "./integrations/calendar-event-response.js";
20
- import { CalendarEventCreatorService } from "./integrations/calendar-event-creator.js";
21
- import { CalendarEventDeleterService } from "./integrations/calendar-event-deleter.js";
22
20
  import { WorkingCadenceService } from "./services/working-cadence.js";
23
21
  import { VERSION, SERVER_NAME } from "./version.js";
22
+ import { createErrorFromCatch } from "./utils/mcp-response.js";
23
+ // Extracted tool handlers
24
+ import { handleCheckSetupStatus, handleStartSetupWizard, handleAnswerWizardQuestion, handleSaveConfig, } from "./tools/setup/index.js";
25
+ import { handleAnalyzeTasks, handleUpdateTaskStatus, handleSyncTasks, handleDetectDuplicates, } from "./tools/tasks/index.js";
26
+ import { handleFindAvailableSlots, handleListCalendarEvents, handleRespondToCalendarEvent, handleRespondToCalendarEventsBatch, handleCreateCalendarEvent, handleDeleteCalendarEvent, handleDeleteCalendarEventsBatch, handleListCalendarSources, handleGetWorkingCadence, } from "./tools/calendar/index.js";
27
+ import { handleSetReminder, handleListTodos, } from "./tools/reminders/index.js";
28
+ import { handleSyncToNotion, handleUpdateConfig, } from "./tools/integrations/index.js";
24
29
  // Global state
25
30
  let config = null;
26
31
  let wizardSession = null;
27
32
  let reminderManager = null;
28
33
  let calendarService = null;
34
+ let googleCalendarService = null;
35
+ let calendarSourceManager = null;
29
36
  let notionService = null;
30
37
  let todoListManager = null;
31
38
  let taskSynchronizer = null;
32
39
  let calendarEventResponseService = null;
33
- let calendarEventCreatorService = null;
34
- let calendarEventDeleterService = null;
35
40
  let workingCadenceService = null;
36
- /**
37
- * Validate config updates for a specific section
38
- */
39
- function validateConfigUpdate(section, updates) {
40
- const invalidFields = [];
41
- switch (section) {
42
- case "user":
43
- if (updates.name !== undefined && typeof updates.name !== "string") {
44
- invalidFields.push("name");
45
- }
46
- if (updates.timezone !== undefined &&
47
- typeof updates.timezone !== "string") {
48
- invalidFields.push("timezone");
49
- }
50
- break;
51
- case "calendar":
52
- if (updates.workingHours !== undefined) {
53
- const wh = updates.workingHours;
54
- if (!wh.start || !wh.end) {
55
- invalidFields.push("workingHours");
56
- }
57
- }
58
- if (updates.deepWorkDays !== undefined &&
59
- !Array.isArray(updates.deepWorkDays)) {
60
- invalidFields.push("deepWorkDays");
61
- }
62
- if (updates.meetingHeavyDays !== undefined &&
63
- !Array.isArray(updates.meetingHeavyDays)) {
64
- invalidFields.push("meetingHeavyDays");
65
- }
66
- break;
67
- case "integrations":
68
- if (updates.notion !== undefined) {
69
- const notion = updates.notion;
70
- if (notion.enabled === true && !notion.databaseId) {
71
- invalidFields.push("notion.databaseId");
72
- }
73
- }
74
- break;
75
- case "team":
76
- if (updates.members !== undefined && !Array.isArray(updates.members)) {
77
- invalidFields.push("members");
78
- }
79
- if (updates.managers !== undefined && !Array.isArray(updates.managers)) {
80
- invalidFields.push("managers");
81
- }
82
- break;
83
- }
84
- if (invalidFields.length > 0) {
85
- return {
86
- valid: false,
87
- error: `無効なフィールド: ${invalidFields.join(", ")}`,
88
- invalidFields,
89
- };
90
- }
91
- return { valid: true };
92
- }
93
- /**
94
- * Apply config updates to a specific section
95
- */
96
- function applyConfigUpdates(currentConfig, section, updates) {
97
- const newConfig = { ...currentConfig };
98
- switch (section) {
99
- case "user":
100
- newConfig.user = { ...newConfig.user, ...updates };
101
- break;
102
- case "calendar":
103
- newConfig.calendar = {
104
- ...newConfig.calendar,
105
- ...updates,
106
- };
107
- break;
108
- case "priorityRules":
109
- newConfig.priorityRules = {
110
- ...newConfig.priorityRules,
111
- ...updates,
112
- };
113
- break;
114
- case "integrations":
115
- // Deep merge for integrations
116
- if (updates.appleReminders) {
117
- newConfig.integrations.appleReminders = {
118
- ...newConfig.integrations.appleReminders,
119
- ...updates.appleReminders,
120
- };
121
- }
122
- if (updates.notion) {
123
- newConfig.integrations.notion = {
124
- ...newConfig.integrations.notion,
125
- ...updates.notion,
126
- };
127
- }
128
- break;
129
- case "team":
130
- newConfig.team = { ...newConfig.team, ...updates };
131
- break;
132
- case "preferences":
133
- newConfig.preferences = {
134
- ...newConfig.preferences,
135
- ...updates,
136
- };
137
- break;
138
- }
139
- return newConfig;
140
- }
141
41
  /**
142
42
  * Initialize services with config
143
43
  */
@@ -149,14 +49,90 @@ function initializeServices(userConfig) {
149
49
  notionDatabaseId: userConfig.integrations.notion.databaseId,
150
50
  });
151
51
  calendarService = new CalendarService();
52
+ // Initialize Google Calendar service if configured
53
+ // Note: GoogleCalendarService requires GoogleOAuthHandler which needs OAuth config
54
+ // For now, we initialize with a stub handler. Full OAuth setup will be done in Task 33.
55
+ try {
56
+ const { GoogleOAuthHandler } = require('./oauth/google-oauth-handler.js');
57
+ const oauthConfig = {
58
+ clientId: process.env.GOOGLE_CLIENT_ID || '',
59
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
60
+ redirectUri: process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/oauth/callback',
61
+ };
62
+ const oauthHandler = new GoogleOAuthHandler(oauthConfig);
63
+ googleCalendarService = new GoogleCalendarService(oauthHandler);
64
+ }
65
+ catch (error) {
66
+ // If Google Calendar initialization fails, continue without it
67
+ console.error('Google Calendar service initialization failed:', error);
68
+ googleCalendarService = null;
69
+ }
70
+ calendarSourceManager = new CalendarSourceManager({
71
+ calendarService,
72
+ googleCalendarService: googleCalendarService || undefined,
73
+ config: userConfig,
74
+ });
152
75
  notionService = new NotionMCPService();
153
76
  todoListManager = new TodoListManager();
154
77
  taskSynchronizer = new TaskSynchronizer();
155
78
  calendarEventResponseService = new CalendarEventResponseService();
156
- calendarEventCreatorService = new CalendarEventCreatorService();
157
- calendarEventDeleterService = new CalendarEventDeleterService();
158
79
  workingCadenceService = new WorkingCadenceService();
159
80
  }
81
+ // ============================================
82
+ // Context Factory Functions
83
+ // ============================================
84
+ function createSetupContext() {
85
+ return {
86
+ getConfig: () => config,
87
+ setConfig: (c) => {
88
+ config = c;
89
+ },
90
+ getWizardSession: () => wizardSession,
91
+ setWizardSession: (session) => {
92
+ wizardSession = session;
93
+ },
94
+ initializeServices,
95
+ };
96
+ }
97
+ function createTaskToolsContext() {
98
+ return {
99
+ getConfig: () => config,
100
+ getTodoListManager: () => todoListManager,
101
+ getTaskSynchronizer: () => taskSynchronizer,
102
+ initializeServices,
103
+ };
104
+ }
105
+ function createCalendarToolsContext() {
106
+ return {
107
+ getConfig: () => config,
108
+ getCalendarSourceManager: () => calendarSourceManager,
109
+ getCalendarEventResponseService: () => calendarEventResponseService,
110
+ getGoogleCalendarService: () => googleCalendarService,
111
+ getWorkingCadenceService: () => workingCadenceService,
112
+ setWorkingCadenceService: (service) => {
113
+ workingCadenceService = service;
114
+ },
115
+ initializeServices,
116
+ };
117
+ }
118
+ function createReminderTodoContext() {
119
+ return {
120
+ getConfig: () => config,
121
+ getReminderManager: () => reminderManager,
122
+ getTodoListManager: () => todoListManager,
123
+ initializeServices,
124
+ };
125
+ }
126
+ function createIntegrationToolsContext() {
127
+ return {
128
+ getConfig: () => config,
129
+ setConfig: (c) => {
130
+ config = c;
131
+ },
132
+ getNotionService: () => notionService,
133
+ initializeServices,
134
+ };
135
+ }
160
136
  /**
161
137
  * Initialize the MCP server with all tools
162
138
  */
@@ -176,257 +152,27 @@ async function createServer() {
176
152
  config = null;
177
153
  }
178
154
  // ============================================
179
- // Setup & Configuration Tools
155
+ // Setup & Configuration Tools - uses extracted handlers
180
156
  // ============================================
181
- /**
182
- * check_setup_status - Check if initial setup is complete
183
- * Requirement: 1.1, 1.2
184
- */
185
- server.tool("check_setup_status", "Check if sage has been configured. Returns setup status and guidance.", {}, async () => {
186
- const exists = await ConfigLoader.exists();
187
- const isValid = config !== null;
188
- if (!exists) {
189
- return {
190
- content: [
191
- {
192
- type: "text",
193
- text: JSON.stringify({
194
- setupComplete: false,
195
- configExists: false,
196
- message: "sageの初期設定が必要です。start_setup_wizardを実行してセットアップを開始してください。",
197
- nextAction: "start_setup_wizard",
198
- }, null, 2),
199
- },
200
- ],
201
- };
202
- }
203
- if (!isValid) {
204
- return {
205
- content: [
206
- {
207
- type: "text",
208
- text: JSON.stringify({
209
- setupComplete: false,
210
- configExists: true,
211
- message: "設定ファイルが見つかりましたが、読み込みに失敗しました。設定を再作成してください。",
212
- nextAction: "start_setup_wizard",
213
- }, null, 2),
214
- },
215
- ],
216
- };
217
- }
218
- return {
219
- content: [
220
- {
221
- type: "text",
222
- text: JSON.stringify({
223
- setupComplete: true,
224
- configExists: true,
225
- userName: config?.user.name,
226
- message: "sageは設定済みです。タスク分析やリマインド設定を開始できます。",
227
- availableTools: [
228
- "analyze_tasks",
229
- "set_reminder",
230
- "find_available_slots",
231
- "sync_to_notion",
232
- "update_config",
233
- ],
234
- }, null, 2),
235
- },
236
- ],
237
- };
238
- });
239
- /**
240
- * start_setup_wizard - Begin the interactive setup process
241
- * Requirement: 1.3
242
- */
157
+ server.tool("check_setup_status", "Check if sage has been configured. Returns setup status and guidance.", {}, async () => handleCheckSetupStatus(createSetupContext()));
243
158
  server.tool("start_setup_wizard", "Start the interactive setup wizard for sage. Returns the first question.", {
244
159
  mode: z
245
160
  .enum(["full", "quick"])
246
161
  .optional()
247
162
  .describe("Setup mode: full (all questions) or quick (essential only)"),
248
- }, async ({ mode = "full" }) => {
249
- wizardSession = SetupWizard.createSession(mode);
250
- const question = SetupWizard.getCurrentQuestion(wizardSession);
251
- return {
252
- content: [
253
- {
254
- type: "text",
255
- text: JSON.stringify({
256
- sessionId: wizardSession.sessionId,
257
- currentStep: wizardSession.currentStep,
258
- totalSteps: wizardSession.totalSteps,
259
- progress: Math.round((wizardSession.currentStep / wizardSession.totalSteps) * 100),
260
- question: {
261
- id: question.id,
262
- text: question.text,
263
- type: question.type,
264
- options: question.options,
265
- defaultValue: question.defaultValue,
266
- helpText: question.helpText,
267
- },
268
- message: "セットアップを開始します。以下の質問に回答してください。",
269
- }, null, 2),
270
- },
271
- ],
272
- };
273
- });
274
- /**
275
- * answer_wizard_question - Answer a setup wizard question
276
- * Requirement: 1.3, 1.4
277
- */
163
+ }, async ({ mode }) => handleStartSetupWizard(createSetupContext(), { mode: mode ?? "full" }));
278
164
  server.tool("answer_wizard_question", "Answer a question in the setup wizard and get the next question.", {
279
165
  questionId: z.string().describe("The ID of the question being answered"),
280
166
  answer: z
281
167
  .union([z.string(), z.array(z.string())])
282
168
  .describe("The answer to the question"),
283
- }, async ({ questionId, answer }) => {
284
- if (!wizardSession) {
285
- return {
286
- content: [
287
- {
288
- type: "text",
289
- text: JSON.stringify({
290
- error: true,
291
- message: "セットアップセッションが見つかりません。start_setup_wizardを実行してください。",
292
- }, null, 2),
293
- },
294
- ],
295
- };
296
- }
297
- const result = SetupWizard.answerQuestion(wizardSession, questionId, answer);
298
- if (!result.success) {
299
- return {
300
- content: [
301
- {
302
- type: "text",
303
- text: JSON.stringify({
304
- error: true,
305
- message: result.error,
306
- currentQuestion: result.currentQuestion,
307
- }, null, 2),
308
- },
309
- ],
310
- };
311
- }
312
- if (result.isComplete) {
313
- return {
314
- content: [
315
- {
316
- type: "text",
317
- text: JSON.stringify({
318
- isComplete: true,
319
- sessionId: wizardSession.sessionId,
320
- answers: wizardSession.answers,
321
- message: "すべての質問に回答しました。save_configを実行して設定を保存してください。",
322
- nextAction: "save_config",
323
- }, null, 2),
324
- },
325
- ],
326
- };
327
- }
328
- const nextQuestion = SetupWizard.getCurrentQuestion(wizardSession);
329
- return {
330
- content: [
331
- {
332
- type: "text",
333
- text: JSON.stringify({
334
- success: true,
335
- currentStep: wizardSession.currentStep,
336
- totalSteps: wizardSession.totalSteps,
337
- progress: Math.round((wizardSession.currentStep / wizardSession.totalSteps) * 100),
338
- question: {
339
- id: nextQuestion.id,
340
- text: nextQuestion.text,
341
- type: nextQuestion.type,
342
- options: nextQuestion.options,
343
- defaultValue: nextQuestion.defaultValue,
344
- helpText: nextQuestion.helpText,
345
- },
346
- }, null, 2),
347
- },
348
- ],
349
- };
350
- });
351
- /**
352
- * save_config - Save the configuration from the setup wizard
353
- * Requirement: 1.4, 1.5, 1.6
354
- */
169
+ }, async ({ questionId, answer }) => handleAnswerWizardQuestion(createSetupContext(), { questionId, answer }));
355
170
  server.tool("save_config", "Save the configuration after completing the setup wizard.", {
356
171
  confirm: z.boolean().describe("Confirm saving the configuration"),
357
- }, async ({ confirm }) => {
358
- if (!confirm) {
359
- return {
360
- content: [
361
- {
362
- type: "text",
363
- text: JSON.stringify({
364
- saved: false,
365
- message: "設定の保存がキャンセルされました。",
366
- }, null, 2),
367
- },
368
- ],
369
- };
370
- }
371
- if (!wizardSession) {
372
- return {
373
- content: [
374
- {
375
- type: "text",
376
- text: JSON.stringify({
377
- error: true,
378
- message: "セットアップセッションが見つかりません。start_setup_wizardを実行してください。",
379
- }, null, 2),
380
- },
381
- ],
382
- };
383
- }
384
- try {
385
- const newConfig = SetupWizard.buildConfig(wizardSession);
386
- await ConfigLoader.save(newConfig);
387
- config = newConfig;
388
- wizardSession = null;
389
- return {
390
- content: [
391
- {
392
- type: "text",
393
- text: JSON.stringify({
394
- saved: true,
395
- configPath: ConfigLoader.getConfigPath(),
396
- userName: newConfig.user.name,
397
- message: `設定を保存しました。${newConfig.user.name}さん、sageをご利用いただきありがとうございます!`,
398
- availableTools: [
399
- "analyze_tasks",
400
- "set_reminder",
401
- "find_available_slots",
402
- "sync_to_notion",
403
- ],
404
- }, null, 2),
405
- },
406
- ],
407
- };
408
- }
409
- catch (error) {
410
- return {
411
- content: [
412
- {
413
- type: "text",
414
- text: JSON.stringify({
415
- error: true,
416
- message: `設定の保存に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
417
- }, null, 2),
418
- },
419
- ],
420
- };
421
- }
422
- });
172
+ }, async ({ confirm }) => handleSaveConfig(createSetupContext(), { confirm }));
423
173
  // ============================================
424
- // Task Analysis Tools (placeholder)
174
+ // Task Analysis Tools - uses extracted handlers
425
175
  // ============================================
426
- /**
427
- * analyze_tasks - Analyze tasks and provide prioritization
428
- * Requirement: 2.1-2.6, 3.1-3.2, 4.1-4.5
429
- */
430
176
  server.tool("analyze_tasks", "Analyze tasks to determine priority, estimate time, and identify stakeholders.", {
431
177
  tasks: z
432
178
  .array(z.object({
@@ -438,63 +184,8 @@ async function createServer() {
438
184
  .describe("Task deadline (ISO 8601 format)"),
439
185
  }))
440
186
  .describe("List of tasks to analyze"),
441
- }, async ({ tasks }) => {
442
- if (!config) {
443
- return {
444
- content: [
445
- {
446
- type: "text",
447
- text: JSON.stringify({
448
- error: true,
449
- message: "sageが設定されていません。check_setup_statusを実行してください。",
450
- }, null, 2),
451
- },
452
- ],
453
- };
454
- }
455
- try {
456
- const result = await TaskAnalyzer.analyzeTasks(tasks, config);
457
- return {
458
- content: [
459
- {
460
- type: "text",
461
- text: JSON.stringify({
462
- success: true,
463
- summary: result.summary,
464
- tasks: result.analyzedTasks.map((t) => ({
465
- title: t.original.title,
466
- description: t.original.description,
467
- deadline: t.original.deadline,
468
- priority: t.priority,
469
- estimatedMinutes: t.estimatedMinutes,
470
- stakeholders: t.stakeholders,
471
- tags: t.tags,
472
- reasoning: t.reasoning,
473
- suggestedReminders: t.suggestedReminders,
474
- })),
475
- }, null, 2),
476
- },
477
- ],
478
- };
479
- }
480
- catch (error) {
481
- return {
482
- content: [
483
- {
484
- type: "text",
485
- text: JSON.stringify({
486
- error: true,
487
- message: `タスク分析に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
488
- }, null, 2),
489
- },
490
- ],
491
- };
492
- }
493
- });
494
- /**
495
- * set_reminder - Set a reminder for a task
496
- * Requirement: 5.1-5.6
497
- */
187
+ }, async ({ tasks }) => handleAnalyzeTasks(createTaskToolsContext(), { tasks }));
188
+ // set_reminder - uses extracted handler
498
189
  server.tool("set_reminder", "Set a reminder for a task in Apple Reminders or Notion.", {
499
190
  taskTitle: z.string().describe("Title of the task"),
500
191
  dueDate: z
@@ -523,108 +214,16 @@ async function createServer() {
523
214
  .string()
524
215
  .optional()
525
216
  .describe("Additional notes for the reminder"),
526
- }, async ({ taskTitle, dueDate, reminderType, list, priority, notes }) => {
527
- if (!config) {
528
- return {
529
- content: [
530
- {
531
- type: "text",
532
- text: JSON.stringify({
533
- error: true,
534
- message: "sageが設定されていません。check_setup_statusを実行してください。",
535
- }, null, 2),
536
- },
537
- ],
538
- };
539
- }
540
- if (!reminderManager) {
541
- initializeServices(config);
542
- }
543
- try {
544
- const result = await reminderManager.setReminder({
545
- taskTitle,
546
- targetDate: dueDate,
547
- reminderType,
548
- list: list ?? config.integrations.appleReminders.defaultList,
549
- priority: priority,
550
- notes,
551
- });
552
- if (result.success) {
553
- // Check if this is a delegation request for Notion
554
- if (result.delegateToNotion && result.notionRequest) {
555
- return {
556
- content: [
557
- {
558
- type: "text",
559
- text: JSON.stringify({
560
- success: true,
561
- destination: "notion_mcp",
562
- method: "delegate",
563
- delegateToNotion: true,
564
- notionRequest: result.notionRequest,
565
- message: `Notionへの追加はClaude Codeが直接notion-create-pagesツールを使用してください。`,
566
- instruction: `notion-create-pagesツールを以下のパラメータで呼び出してください:
567
- - parent: { "type": "data_source_id", "data_source_id": "${result.notionRequest.databaseId.replace(/-/g, "")}" }
568
- - pages: [{ "properties": ${JSON.stringify(result.notionRequest.properties)} }]`,
569
- }, null, 2),
570
- },
571
- ],
572
- };
573
- }
574
- return {
575
- content: [
576
- {
577
- type: "text",
578
- text: JSON.stringify({
579
- success: true,
580
- destination: result.destination,
581
- method: result.method,
582
- reminderId: result.reminderId,
583
- reminderUrl: result.reminderUrl ?? result.pageUrl,
584
- message: result.destination === "apple_reminders"
585
- ? `Apple Remindersにリマインダーを作成しました: ${taskTitle}`
586
- : `Notionにタスクを作成しました: ${taskTitle}`,
587
- }, null, 2),
588
- },
589
- ],
590
- };
591
- }
592
- return {
593
- content: [
594
- {
595
- type: "text",
596
- text: JSON.stringify({
597
- success: false,
598
- destination: result.destination,
599
- error: result.error,
600
- fallbackText: result.fallbackText,
601
- message: result.fallbackText
602
- ? "自動作成に失敗しました。以下のテキストを手動でコピーしてください。"
603
- : `リマインダー作成に失敗しました: ${result.error}`,
604
- }, null, 2),
605
- },
606
- ],
607
- };
608
- }
609
- catch (error) {
610
- return {
611
- content: [
612
- {
613
- type: "text",
614
- text: JSON.stringify({
615
- error: true,
616
- message: `リマインダー設定に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
617
- }, null, 2),
618
- },
619
- ],
620
- };
621
- }
622
- });
623
- /**
624
- * find_available_slots - Find available time slots in calendar
625
- * Requirement: 3.3-3.6, 6.1-6.6
626
- */
627
- server.tool("find_available_slots", "Find available time slots in the calendar for scheduling tasks.", {
217
+ }, async ({ taskTitle, dueDate, reminderType, list, priority, notes }) => handleSetReminder(createReminderTodoContext(), {
218
+ taskTitle,
219
+ dueDate,
220
+ reminderType,
221
+ list,
222
+ priority,
223
+ notes,
224
+ }));
225
+ // find_available_slots - uses extracted handler
226
+ server.tool("find_available_slots", "Find available time slots in the calendar for scheduling tasks from all enabled calendar sources.", {
628
227
  durationMinutes: z.number().describe("Required duration in minutes"),
629
228
  startDate: z
630
229
  .string()
@@ -638,217 +237,41 @@ async function createServer() {
638
237
  .boolean()
639
238
  .optional()
640
239
  .describe("Prefer deep work time slots"),
641
- }, async ({ durationMinutes, startDate, endDate, preferDeepWork }) => {
642
- if (!config) {
643
- return {
644
- content: [
645
- {
646
- type: "text",
647
- text: JSON.stringify({
648
- error: true,
649
- message: "sageが設定されていません。check_setup_statusを実行してください。",
650
- }, null, 2),
651
- },
652
- ],
653
- };
654
- }
655
- if (!calendarService) {
656
- initializeServices(config);
657
- }
658
- try {
659
- // Check platform availability
660
- const platformInfo = await calendarService.detectPlatform();
661
- const isAvailable = await calendarService.isAvailable();
662
- if (!isAvailable) {
663
- // Return manual input prompt for unsupported platforms
664
- const manualPrompt = calendarService.generateManualInputPrompt(startDate ?? new Date().toISOString().split("T")[0], endDate ??
665
- new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
666
- .toISOString()
667
- .split("T")[0]);
668
- return {
669
- content: [
670
- {
671
- type: "text",
672
- text: JSON.stringify({
673
- success: false,
674
- platform: platformInfo.platform,
675
- method: platformInfo.recommendedMethod,
676
- message: "カレンダー統合がこのプラットフォームで利用できません。手動で予定を入力してください。",
677
- manualPrompt,
678
- }, null, 2),
679
- },
680
- ],
681
- };
682
- }
683
- // Fetch events from calendar
684
- const searchStart = startDate ?? new Date().toISOString().split("T")[0];
685
- const searchEnd = endDate ??
686
- new Date(Date.now() + 7 * 24 * 60 * 60 * 1000)
687
- .toISOString()
688
- .split("T")[0];
689
- const events = await calendarService.fetchEvents(searchStart, searchEnd);
690
- // Find available slots
691
- const workingHours = {
692
- start: config.calendar.workingHours.start,
693
- end: config.calendar.workingHours.end,
694
- };
695
- const slots = calendarService.findAvailableSlotsFromEvents(events, durationMinutes, workingHours, searchStart);
696
- // Apply suitability scoring
697
- const suitabilityConfig = {
698
- deepWorkDays: config.calendar.deepWorkDays,
699
- meetingHeavyDays: config.calendar.meetingHeavyDays,
700
- };
701
- const scoredSlots = slots.map((slot) => calendarService.calculateSuitability(slot, suitabilityConfig));
702
- // Filter for deep work preference if requested
703
- const filteredSlots = preferDeepWork
704
- ? scoredSlots.filter((s) => s.dayType === "deep-work")
705
- : scoredSlots;
706
- // Sort by suitability (excellent > good > acceptable)
707
- const suitabilityOrder = { excellent: 0, good: 1, acceptable: 2 };
708
- filteredSlots.sort((a, b) => suitabilityOrder[a.suitability] - suitabilityOrder[b.suitability]);
709
- return {
710
- content: [
711
- {
712
- type: "text",
713
- text: JSON.stringify({
714
- success: true,
715
- platform: platformInfo.platform,
716
- method: platformInfo.recommendedMethod,
717
- searchRange: { start: searchStart, end: searchEnd },
718
- eventsFound: events.length,
719
- slots: filteredSlots.slice(0, 10).map((slot) => ({
720
- start: slot.start,
721
- end: slot.end,
722
- durationMinutes: slot.durationMinutes,
723
- suitability: slot.suitability,
724
- dayType: slot.dayType,
725
- reason: slot.reason,
726
- })),
727
- message: filteredSlots.length > 0
728
- ? `${filteredSlots.length}件の空き時間が見つかりました。`
729
- : "指定した条件に合う空き時間が見つかりませんでした。",
730
- }, null, 2),
731
- },
732
- ],
733
- };
734
- }
735
- catch (error) {
736
- return {
737
- content: [
738
- {
739
- type: "text",
740
- text: JSON.stringify({
741
- error: true,
742
- message: `カレンダー検索に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
743
- }, null, 2),
744
- },
745
- ],
746
- };
747
- }
748
- });
749
- /**
750
- * list_calendar_events - List calendar events for a specified period
751
- * Requirement: 16.1-16.12
752
- */
753
- server.tool("list_calendar_events", "List calendar events for a specified period. Returns events with details including calendar name and location.", {
240
+ minDurationMinutes: z
241
+ .number()
242
+ .optional()
243
+ .describe("Minimum slot duration in minutes (default: 25)"),
244
+ maxDurationMinutes: z
245
+ .number()
246
+ .optional()
247
+ .describe("Maximum slot duration in minutes (default: 480)"),
248
+ }, async ({ durationMinutes, startDate, endDate, preferDeepWork, minDurationMinutes, maxDurationMinutes }) => handleFindAvailableSlots(createCalendarToolsContext(), {
249
+ durationMinutes,
250
+ startDate,
251
+ endDate,
252
+ preferDeepWork,
253
+ minDurationMinutes,
254
+ maxDurationMinutes,
255
+ }));
256
+ // list_calendar_events - uses extracted handler
257
+ server.tool("list_calendar_events", "List calendar events for a specified period from enabled sources (EventKit, Google Calendar, or both). Returns events with details including calendar name and location.", {
754
258
  startDate: z
755
259
  .string()
756
260
  .describe("Start date in ISO 8601 format (e.g., 2025-01-15)"),
757
261
  endDate: z
758
262
  .string()
759
263
  .describe("End date in ISO 8601 format (e.g., 2025-01-20)"),
760
- calendarName: z
264
+ calendarId: z
761
265
  .string()
762
266
  .optional()
763
- .describe("Optional: filter events by calendar name"),
764
- }, async ({ startDate, endDate, calendarName }) => {
765
- if (!config) {
766
- return {
767
- content: [
768
- {
769
- type: "text",
770
- text: JSON.stringify({
771
- error: true,
772
- message: "sageが設定されていません。check_setup_statusを実行してください。",
773
- }, null, 2),
774
- },
775
- ],
776
- };
777
- }
778
- if (!calendarService) {
779
- initializeServices(config);
780
- }
781
- try {
782
- // Check platform availability
783
- const platformInfo = await calendarService.detectPlatform();
784
- const isAvailable = await calendarService.isAvailable();
785
- if (!isAvailable) {
786
- return {
787
- content: [
788
- {
789
- type: "text",
790
- text: JSON.stringify({
791
- success: false,
792
- platform: platformInfo.platform,
793
- method: platformInfo.recommendedMethod,
794
- message: "カレンダー統合がこのプラットフォームで利用できません。macOSで実行してください。",
795
- }, null, 2),
796
- },
797
- ],
798
- };
799
- }
800
- // List events
801
- const result = await calendarService.listEvents({
802
- startDate,
803
- endDate,
804
- calendarName,
805
- });
806
- return {
807
- content: [
808
- {
809
- type: "text",
810
- text: JSON.stringify({
811
- success: true,
812
- platform: platformInfo.platform,
813
- method: platformInfo.recommendedMethod,
814
- events: result.events.map((event) => ({
815
- id: event.id,
816
- title: event.title,
817
- start: event.start,
818
- end: event.end,
819
- isAllDay: event.isAllDay,
820
- calendar: event.calendar,
821
- location: event.location,
822
- })),
823
- period: result.period,
824
- totalEvents: result.totalEvents,
825
- message: result.totalEvents > 0
826
- ? `${result.totalEvents}件のイベントが見つかりました。`
827
- : "指定した期間にイベントが見つかりませんでした。",
828
- }, null, 2),
829
- },
830
- ],
831
- };
832
- }
833
- catch (error) {
834
- return {
835
- content: [
836
- {
837
- type: "text",
838
- text: JSON.stringify({
839
- error: true,
840
- message: `カレンダーイベントの取得に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
841
- }, null, 2),
842
- },
843
- ],
844
- };
845
- }
846
- });
847
- /**
848
- * respond_to_calendar_event - Respond to a single calendar event
849
- * Requirement: 17.1, 17.2, 17.5-17.11
850
- */
851
- server.tool("respond_to_calendar_event", "Respond to a calendar event with accept, decline, or tentative. Use this to RSVP to meeting invitations.", {
267
+ .describe("Optional: filter events by calendar ID or name"),
268
+ }, async ({ startDate, endDate, calendarId }) => handleListCalendarEvents(createCalendarToolsContext(), {
269
+ startDate,
270
+ endDate,
271
+ calendarId,
272
+ }));
273
+ // respond_to_calendar_event - uses extracted handler
274
+ server.tool("respond_to_calendar_event", "Respond to a calendar event with accept, decline, or tentative. Supports both EventKit (macOS) and Google Calendar events. Use this to RSVP to meeting invitations from any enabled calendar source.", {
852
275
  eventId: z.string().describe("The ID of the calendar event to respond to"),
853
276
  response: z
854
277
  .enum(["accept", "decline", "tentative"])
@@ -856,102 +279,23 @@ async function createServer() {
856
279
  comment: z
857
280
  .string()
858
281
  .optional()
859
- .describe("Optional comment to include with the response (e.g., '年末年始休暇のため')"),
860
- }, async ({ eventId, response, comment }) => {
861
- if (!config) {
862
- return {
863
- content: [
864
- {
865
- type: "text",
866
- text: JSON.stringify({
867
- error: true,
868
- message: "sageが設定されていません。check_setup_statusを実行してください。",
869
- }, null, 2),
870
- },
871
- ],
872
- };
873
- }
874
- if (!calendarEventResponseService) {
875
- initializeServices(config);
876
- }
877
- try {
878
- // Check platform availability
879
- const isAvailable = await calendarEventResponseService.isEventKitAvailable();
880
- if (!isAvailable) {
881
- return {
882
- content: [
883
- {
884
- type: "text",
885
- text: JSON.stringify({
886
- success: false,
887
- message: "カレンダーイベント返信機能はmacOSでのみ利用可能です。",
888
- }, null, 2),
889
- },
890
- ],
891
- };
892
- }
893
- // Respond to the event
894
- const result = await calendarEventResponseService.respondToEvent({
895
- eventId,
896
- response,
897
- comment,
898
- });
899
- if (result.success) {
900
- return {
901
- content: [
902
- {
903
- type: "text",
904
- text: JSON.stringify({
905
- success: true,
906
- eventId: result.eventId,
907
- eventTitle: result.eventTitle,
908
- newStatus: result.newStatus,
909
- method: result.method,
910
- instanceOnly: result.instanceOnly,
911
- message: result.message,
912
- }, null, 2),
913
- },
914
- ],
915
- };
916
- }
917
- // Handle skipped or failed response
918
- return {
919
- content: [
920
- {
921
- type: "text",
922
- text: JSON.stringify({
923
- success: false,
924
- eventId: result.eventId,
925
- eventTitle: result.eventTitle,
926
- skipped: result.skipped,
927
- reason: result.reason,
928
- error: result.error,
929
- message: result.skipped
930
- ? `イベントをスキップしました: ${result.reason}`
931
- : `イベント返信に失敗しました: ${result.error}`,
932
- }, null, 2),
933
- },
934
- ],
935
- };
936
- }
937
- catch (error) {
938
- return {
939
- content: [
940
- {
941
- type: "text",
942
- text: JSON.stringify({
943
- error: true,
944
- message: `カレンダーイベント返信に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
945
- }, null, 2),
946
- },
947
- ],
948
- };
949
- }
950
- });
951
- /**
952
- * respond_to_calendar_events_batch - Respond to multiple calendar events
953
- * Requirement: 17.3, 17.4, 17.12
954
- */
282
+ .describe("Optional comment to include with the response (e.g., '年末年始休暇のため'). Note: Comments are only supported for EventKit events."),
283
+ source: z
284
+ .enum(["eventkit", "google"])
285
+ .optional()
286
+ .describe("Optional: Specify the calendar source explicitly. If not provided, will try Google Calendar first, then EventKit."),
287
+ calendarId: z
288
+ .string()
289
+ .optional()
290
+ .describe("Optional: Google Calendar ID (defaults to 'primary'). Only used for Google Calendar events."),
291
+ }, async ({ eventId, response, comment, source, calendarId }) => handleRespondToCalendarEvent(createCalendarToolsContext(), {
292
+ eventId,
293
+ response,
294
+ comment,
295
+ source,
296
+ calendarId,
297
+ }));
298
+ // respond_to_calendar_events_batch - uses extracted handler
955
299
  server.tool("respond_to_calendar_events_batch", "Respond to multiple calendar events at once. Useful for declining all events during vacation or leave periods.", {
956
300
  eventIds: z
957
301
  .array(z.string())
@@ -963,82 +307,13 @@ async function createServer() {
963
307
  .string()
964
308
  .optional()
965
309
  .describe("Optional comment to include with all responses (e.g., '年末年始休暇のため')"),
966
- }, async ({ eventIds, response, comment }) => {
967
- if (!config) {
968
- return {
969
- content: [
970
- {
971
- type: "text",
972
- text: JSON.stringify({
973
- error: true,
974
- message: "sageが設定されていません。check_setup_statusを実行してください。",
975
- }, null, 2),
976
- },
977
- ],
978
- };
979
- }
980
- if (!calendarEventResponseService) {
981
- initializeServices(config);
982
- }
983
- try {
984
- // Check platform availability
985
- const isAvailable = await calendarEventResponseService.isEventKitAvailable();
986
- if (!isAvailable) {
987
- return {
988
- content: [
989
- {
990
- type: "text",
991
- text: JSON.stringify({
992
- success: false,
993
- message: "カレンダーイベント返信機能はmacOSでのみ利用可能です。",
994
- }, null, 2),
995
- },
996
- ],
997
- };
998
- }
999
- // Respond to all events in batch
1000
- const result = await calendarEventResponseService.respondToEventsBatch({
1001
- eventIds,
1002
- response,
1003
- comment,
1004
- });
1005
- return {
1006
- content: [
1007
- {
1008
- type: "text",
1009
- text: JSON.stringify({
1010
- success: result.success,
1011
- summary: result.summary,
1012
- details: {
1013
- succeeded: result.details.succeeded,
1014
- skipped: result.details.skipped,
1015
- failed: result.details.failed,
1016
- },
1017
- message: result.message,
1018
- }, null, 2),
1019
- },
1020
- ],
1021
- };
1022
- }
1023
- catch (error) {
1024
- return {
1025
- content: [
1026
- {
1027
- type: "text",
1028
- text: JSON.stringify({
1029
- error: true,
1030
- message: `カレンダーイベント一括返信に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1031
- }, null, 2),
1032
- },
1033
- ],
1034
- };
1035
- }
1036
- });
1037
- /**
1038
- * create_calendar_event - Create a new calendar event
1039
- * Requirement: 18.1-18.11
1040
- */
1041
- server.tool("create_calendar_event", "Create a new calendar event with optional location, notes, and alarms.", {
310
+ }, async ({ eventIds, response, comment }) => handleRespondToCalendarEventsBatch(createCalendarToolsContext(), {
311
+ eventIds,
312
+ response,
313
+ comment,
314
+ }));
315
+ // create_calendar_event - uses extracted handler
316
+ server.tool("create_calendar_event", "Create a new calendar event in the appropriate calendar source with optional location, notes, and alarms.", {
1042
317
  title: z.string().describe("Event title"),
1043
318
  startDate: z
1044
319
  .string()
@@ -1056,429 +331,62 @@ async function createServer() {
1056
331
  .array(z.string())
1057
332
  .optional()
1058
333
  .describe("Optional: Override default alarms with custom settings (e.g., ['-15m', '-1h']). If omitted, calendar's default alarm settings apply."),
1059
- }, async ({ title, startDate, endDate, location, notes, calendarName, alarms }) => {
1060
- if (!config) {
1061
- return {
1062
- content: [
1063
- {
1064
- type: "text",
1065
- text: JSON.stringify({
1066
- error: true,
1067
- message: "sageが設定されていません。check_setup_statusを実行してください。",
1068
- }, null, 2),
1069
- },
1070
- ],
1071
- };
1072
- }
1073
- if (!calendarEventCreatorService) {
1074
- initializeServices(config);
1075
- }
1076
- try {
1077
- // Check platform availability
1078
- const isAvailable = await calendarEventCreatorService.isEventKitAvailable();
1079
- if (!isAvailable) {
1080
- return {
1081
- content: [
1082
- {
1083
- type: "text",
1084
- text: JSON.stringify({
1085
- success: false,
1086
- message: "カレンダーイベント作成機能はmacOSでのみ利用可能です。",
1087
- }, null, 2),
1088
- },
1089
- ],
1090
- };
1091
- }
1092
- // Create the event
1093
- const result = await calendarEventCreatorService.createEvent({
1094
- title,
1095
- startDate,
1096
- endDate,
1097
- location,
1098
- notes,
1099
- calendarName,
1100
- alarms,
1101
- });
1102
- if (result.success) {
1103
- return {
1104
- content: [
1105
- {
1106
- type: "text",
1107
- text: JSON.stringify({
1108
- success: true,
1109
- eventId: result.eventId,
1110
- title: result.title,
1111
- startDate: result.startDate,
1112
- endDate: result.endDate,
1113
- calendarName: result.calendarName,
1114
- isAllDay: result.isAllDay,
1115
- message: result.message,
1116
- }, null, 2),
1117
- },
1118
- ],
1119
- };
1120
- }
1121
- // Handle creation failure
1122
- return {
1123
- content: [
1124
- {
1125
- type: "text",
1126
- text: JSON.stringify({
1127
- success: false,
1128
- error: result.error,
1129
- message: result.message,
1130
- }, null, 2),
1131
- },
1132
- ],
1133
- };
1134
- }
1135
- catch (error) {
1136
- return {
1137
- content: [
1138
- {
1139
- type: "text",
1140
- text: JSON.stringify({
1141
- error: true,
1142
- message: `カレンダーイベント作成に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1143
- }, null, 2),
1144
- },
1145
- ],
1146
- };
1147
- }
1148
- });
1149
- /**
1150
- * delete_calendar_event - Delete a calendar event
1151
- * Requirement: 19.1-19.9
1152
- */
1153
- server.tool("delete_calendar_event", "Delete a calendar event by its ID.", {
334
+ preferredSource: z
335
+ .enum(['eventkit', 'google'])
336
+ .optional()
337
+ .describe("Preferred calendar source to create the event in. If not specified, uses the first enabled source."),
338
+ }, async ({ title, startDate, endDate, location, notes, calendarName, alarms, preferredSource }) => handleCreateCalendarEvent(createCalendarToolsContext(), {
339
+ title,
340
+ startDate,
341
+ endDate,
342
+ location,
343
+ notes,
344
+ calendarName,
345
+ alarms,
346
+ preferredSource,
347
+ }));
348
+ // delete_calendar_event - uses extracted handler
349
+ server.tool("delete_calendar_event", "Delete a calendar event from enabled calendar sources by its ID. If source not specified, attempts deletion from all enabled sources.", {
1154
350
  eventId: z.string().describe("Event ID (UUID or full ID from list_calendar_events)"),
1155
- calendarName: z
1156
- .string()
351
+ source: z
352
+ .enum(['eventkit', 'google'])
1157
353
  .optional()
1158
- .describe("Calendar name (searches all calendars if not specified)"),
1159
- }, async ({ eventId, calendarName }) => {
1160
- if (!config) {
1161
- return {
1162
- content: [
1163
- {
1164
- type: "text",
1165
- text: JSON.stringify({
1166
- error: true,
1167
- message: "sageが設定されていません。check_setup_statusを実行してください。",
1168
- }, null, 2),
1169
- },
1170
- ],
1171
- };
1172
- }
1173
- if (!calendarEventDeleterService) {
1174
- initializeServices(config);
1175
- }
1176
- try {
1177
- // Check platform availability
1178
- const isAvailable = await calendarEventDeleterService.isEventKitAvailable();
1179
- if (!isAvailable) {
1180
- return {
1181
- content: [
1182
- {
1183
- type: "text",
1184
- text: JSON.stringify({
1185
- error: true,
1186
- message: "カレンダー統合がこのプラットフォームで利用できません。macOSで実行してください。",
1187
- }, null, 2),
1188
- },
1189
- ],
1190
- };
1191
- }
1192
- // Delete the event
1193
- const result = await calendarEventDeleterService.deleteEvent({
1194
- eventId,
1195
- calendarName,
1196
- });
1197
- if (result.success) {
1198
- return {
1199
- content: [
1200
- {
1201
- type: "text",
1202
- text: JSON.stringify({
1203
- success: true,
1204
- eventId: result.eventId,
1205
- title: result.title,
1206
- calendarName: result.calendarName,
1207
- message: result.message,
1208
- }, null, 2),
1209
- },
1210
- ],
1211
- };
1212
- }
1213
- // Handle deletion failure
1214
- return {
1215
- content: [
1216
- {
1217
- type: "text",
1218
- text: JSON.stringify({
1219
- success: false,
1220
- error: result.error,
1221
- message: result.message,
1222
- }, null, 2),
1223
- },
1224
- ],
1225
- };
1226
- }
1227
- catch (error) {
1228
- return {
1229
- content: [
1230
- {
1231
- type: "text",
1232
- text: JSON.stringify({
1233
- error: true,
1234
- message: `カレンダーイベント削除に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1235
- }, null, 2),
1236
- },
1237
- ],
1238
- };
1239
- }
1240
- });
1241
- /**
1242
- * delete_calendar_events_batch - Delete multiple calendar events
1243
- * Requirement: 19.10-19.11
1244
- */
1245
- server.tool("delete_calendar_events_batch", "Delete multiple calendar events by their IDs.", {
354
+ .describe("Calendar source to delete from. If not specified, attempts deletion from all enabled sources."),
355
+ }, async ({ eventId, source }) => handleDeleteCalendarEvent(createCalendarToolsContext(), { eventId, source }));
356
+ // delete_calendar_events_batch - uses extracted handler
357
+ server.tool("delete_calendar_events_batch", "Delete multiple calendar events from enabled calendar sources by their IDs. If source not specified, attempts deletion from all enabled sources.", {
1246
358
  eventIds: z.array(z.string()).describe("Array of event IDs to delete"),
1247
- calendarName: z
1248
- .string()
359
+ source: z
360
+ .enum(['eventkit', 'google'])
1249
361
  .optional()
1250
- .describe("Calendar name (searches all calendars if not specified)"),
1251
- }, async ({ eventIds, calendarName }) => {
1252
- if (!config) {
1253
- return {
1254
- content: [
1255
- {
1256
- type: "text",
1257
- text: JSON.stringify({
1258
- error: true,
1259
- message: "sageが設定されていません。check_setup_statusを実行してください。",
1260
- }, null, 2),
1261
- },
1262
- ],
1263
- };
1264
- }
1265
- if (!calendarEventDeleterService) {
1266
- initializeServices(config);
1267
- }
1268
- try {
1269
- // Check platform availability
1270
- const isAvailable = await calendarEventDeleterService.isEventKitAvailable();
1271
- if (!isAvailable) {
1272
- return {
1273
- content: [
1274
- {
1275
- type: "text",
1276
- text: JSON.stringify({
1277
- error: true,
1278
- message: "カレンダー統合がこのプラットフォームで利用できません。macOSで実行してください。",
1279
- }, null, 2),
1280
- },
1281
- ],
1282
- };
1283
- }
1284
- // Delete events in batch
1285
- const result = await calendarEventDeleterService.deleteEventsBatch({
1286
- eventIds,
1287
- calendarName,
1288
- });
1289
- return {
1290
- content: [
1291
- {
1292
- type: "text",
1293
- text: JSON.stringify({
1294
- success: result.success,
1295
- totalCount: result.totalCount,
1296
- successCount: result.successCount,
1297
- failedCount: result.failedCount,
1298
- results: result.results.map((r) => ({
1299
- eventId: r.eventId,
1300
- success: r.success,
1301
- title: r.title,
1302
- calendarName: r.calendarName,
1303
- error: r.error,
1304
- })),
1305
- message: result.message,
1306
- }, null, 2),
1307
- },
1308
- ],
1309
- };
1310
- }
1311
- catch (error) {
1312
- return {
1313
- content: [
1314
- {
1315
- type: "text",
1316
- text: JSON.stringify({
1317
- error: true,
1318
- message: `カレンダーイベント一括削除に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1319
- }, null, 2),
1320
- },
1321
- ],
1322
- };
1323
- }
1324
- });
1325
- /**
1326
- * sync_to_notion - Sync a task to Notion
1327
- * Requirement: 8.1-8.5
1328
- */
362
+ .describe("Calendar source to delete from. If not specified, attempts deletion from all enabled sources."),
363
+ }, async ({ eventIds, source }) => handleDeleteCalendarEventsBatch(createCalendarToolsContext(), { eventIds, source }));
364
+ // sync_to_notion - uses extracted handler
1329
365
  server.tool("sync_to_notion", "Sync a task to Notion database for long-term tracking.", {
1330
366
  taskTitle: z.string().describe("Title of the task"),
1331
- description: z.string().optional().describe("Task description"),
1332
- priority: z
1333
- .enum(["P0", "P1", "P2", "P3"])
1334
- .optional()
1335
- .describe("Task priority"),
1336
- dueDate: z.string().optional().describe("Due date (ISO 8601 format)"),
1337
- stakeholders: z
1338
- .array(z.string())
1339
- .optional()
1340
- .describe("List of stakeholders"),
1341
- estimatedMinutes: z
1342
- .number()
1343
- .optional()
1344
- .describe("Estimated duration in minutes"),
1345
- }, async ({ taskTitle, description, priority, dueDate, stakeholders, estimatedMinutes, }) => {
1346
- if (!config) {
1347
- return {
1348
- content: [
1349
- {
1350
- type: "text",
1351
- text: JSON.stringify({
1352
- error: true,
1353
- message: "sageが設定されていません。check_setup_statusを実行してください。",
1354
- }, null, 2),
1355
- },
1356
- ],
1357
- };
1358
- }
1359
- if (!config.integrations.notion.enabled) {
1360
- return {
1361
- content: [
1362
- {
1363
- type: "text",
1364
- text: JSON.stringify({
1365
- error: true,
1366
- message: "Notion統合が有効になっていません。update_configでNotion設定を更新してください。",
1367
- }, null, 2),
1368
- },
1369
- ],
1370
- };
1371
- }
1372
- if (!notionService) {
1373
- initializeServices(config);
1374
- }
1375
- try {
1376
- // Check if Notion MCP is available
1377
- const isAvailable = await notionService.isAvailable();
1378
- // Build properties for Notion page
1379
- const properties = notionService.buildNotionProperties({
1380
- title: taskTitle,
1381
- priority,
1382
- deadline: dueDate,
1383
- stakeholders,
1384
- estimatedMinutes,
1385
- description,
1386
- });
1387
- if (!isAvailable) {
1388
- // Generate fallback template for manual copy
1389
- const fallbackText = notionService.generateFallbackTemplate({
1390
- title: taskTitle,
1391
- priority,
1392
- deadline: dueDate,
1393
- stakeholders,
1394
- estimatedMinutes,
1395
- description,
1396
- });
1397
- return {
1398
- content: [
1399
- {
1400
- type: "text",
1401
- text: JSON.stringify({
1402
- success: false,
1403
- method: "fallback",
1404
- message: "Notion MCP統合が利用できません。以下のテンプレートを手動でNotionにコピーしてください。",
1405
- fallbackText,
1406
- task: {
1407
- taskTitle,
1408
- priority: priority ?? "P3",
1409
- dueDate,
1410
- stakeholders: stakeholders ?? [],
1411
- estimatedMinutes,
1412
- },
1413
- }, null, 2),
1414
- },
1415
- ],
1416
- };
1417
- }
1418
- // Create page in Notion via MCP
1419
- const result = await notionService.createPage({
1420
- databaseId: config.integrations.notion.databaseId,
1421
- title: taskTitle,
1422
- properties,
1423
- });
1424
- if (result.success) {
1425
- return {
1426
- content: [
1427
- {
1428
- type: "text",
1429
- text: JSON.stringify({
1430
- success: true,
1431
- method: "mcp",
1432
- pageId: result.pageId,
1433
- pageUrl: result.pageUrl,
1434
- message: `Notionにタスクを同期しました: ${taskTitle}`,
1435
- }, null, 2),
1436
- },
1437
- ],
1438
- };
1439
- }
1440
- // MCP call failed, provide fallback
1441
- const fallbackText = notionService.generateFallbackTemplate({
1442
- title: taskTitle,
1443
- priority,
1444
- deadline: dueDate,
1445
- stakeholders,
1446
- estimatedMinutes,
1447
- description,
1448
- });
1449
- return {
1450
- content: [
1451
- {
1452
- type: "text",
1453
- text: JSON.stringify({
1454
- success: false,
1455
- method: "fallback",
1456
- error: result.error,
1457
- message: "Notion MCP呼び出しに失敗しました。以下のテンプレートを手動でコピーしてください。",
1458
- fallbackText,
1459
- }, null, 2),
1460
- },
1461
- ],
1462
- };
1463
- }
1464
- catch (error) {
1465
- return {
1466
- content: [
1467
- {
1468
- type: "text",
1469
- text: JSON.stringify({
1470
- error: true,
1471
- message: `Notion同期に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1472
- }, null, 2),
1473
- },
1474
- ],
1475
- };
1476
- }
1477
- });
1478
- /**
1479
- * update_config - Update configuration
1480
- * Requirement: 10.1-10.6
1481
- */
367
+ description: z.string().optional().describe("Task description"),
368
+ priority: z
369
+ .enum(["P0", "P1", "P2", "P3"])
370
+ .optional()
371
+ .describe("Task priority"),
372
+ dueDate: z.string().optional().describe("Due date (ISO 8601 format)"),
373
+ stakeholders: z
374
+ .array(z.string())
375
+ .optional()
376
+ .describe("List of stakeholders"),
377
+ estimatedMinutes: z
378
+ .number()
379
+ .optional()
380
+ .describe("Estimated duration in minutes"),
381
+ }, async ({ taskTitle, description, priority, dueDate, stakeholders, estimatedMinutes }) => handleSyncToNotion(createIntegrationToolsContext(), {
382
+ taskTitle,
383
+ description,
384
+ priority,
385
+ dueDate,
386
+ stakeholders,
387
+ estimatedMinutes,
388
+ }));
389
+ // update_config - uses extracted handler
1482
390
  server.tool("update_config", "Update sage configuration settings.", {
1483
391
  section: z
1484
392
  .enum([
@@ -1491,81 +399,13 @@ async function createServer() {
1491
399
  ])
1492
400
  .describe("Configuration section to update"),
1493
401
  updates: z.record(z.unknown()).describe("Key-value pairs to update"),
1494
- }, async ({ section, updates }) => {
1495
- if (!config) {
1496
- return {
1497
- content: [
1498
- {
1499
- type: "text",
1500
- text: JSON.stringify({
1501
- error: true,
1502
- message: "sageが設定されていません。check_setup_statusを実行してください。",
1503
- }, null, 2),
1504
- },
1505
- ],
1506
- };
1507
- }
1508
- try {
1509
- // Validate section-specific updates
1510
- const validationResult = validateConfigUpdate(section, updates);
1511
- if (!validationResult.valid) {
1512
- return {
1513
- content: [
1514
- {
1515
- type: "text",
1516
- text: JSON.stringify({
1517
- error: true,
1518
- message: `設定の検証に失敗しました: ${validationResult.error}`,
1519
- invalidFields: validationResult.invalidFields,
1520
- }, null, 2),
1521
- },
1522
- ],
1523
- };
1524
- }
1525
- // Apply updates to config
1526
- const updatedConfig = applyConfigUpdates(config, section, updates);
1527
- // Save the updated config
1528
- await ConfigLoader.save(updatedConfig);
1529
- config = updatedConfig;
1530
- // Re-initialize services if integrations changed
1531
- if (section === "integrations") {
1532
- initializeServices(config);
1533
- }
1534
- return {
1535
- content: [
1536
- {
1537
- type: "text",
1538
- text: JSON.stringify({
1539
- success: true,
1540
- section,
1541
- updatedFields: Object.keys(updates),
1542
- message: `設定を更新しました: ${section}`,
1543
- }, null, 2),
1544
- },
1545
- ],
1546
- };
1547
- }
1548
- catch (error) {
1549
- return {
1550
- content: [
1551
- {
1552
- type: "text",
1553
- text: JSON.stringify({
1554
- error: true,
1555
- message: `設定の更新に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1556
- }, null, 2),
1557
- },
1558
- ],
1559
- };
1560
- }
1561
- });
402
+ }, async ({ section, updates }) => handleUpdateConfig(createIntegrationToolsContext(), {
403
+ section,
404
+ updates: updates,
405
+ }));
1562
406
  // ============================================
1563
- // TODO List Management Tools
407
+ // TODO List Management Tools - uses extracted handlers
1564
408
  // ============================================
1565
- /**
1566
- * list_todos - List all TODO items with optional filtering
1567
- * Requirement: 12.1, 12.2, 12.3, 12.4, 12.7, 12.8
1568
- */
1569
409
  server.tool("list_todos", "List TODO items from Apple Reminders and Notion with optional filtering.", {
1570
410
  priority: z
1571
411
  .array(z.enum(["P0", "P1", "P2", "P3"]))
@@ -1581,89 +421,14 @@ async function createServer() {
1581
421
  .describe("Filter by source"),
1582
422
  todayOnly: z.boolean().optional().describe("Show only tasks due today"),
1583
423
  tags: z.array(z.string()).optional().describe("Filter by tags"),
1584
- }, async ({ priority, status, source, todayOnly, tags }) => {
1585
- if (!config) {
1586
- return {
1587
- content: [
1588
- {
1589
- type: "text",
1590
- text: JSON.stringify({
1591
- error: true,
1592
- message: "sageが設定されていません。check_setup_statusを実行してください。",
1593
- }, null, 2),
1594
- },
1595
- ],
1596
- };
1597
- }
1598
- if (!todoListManager) {
1599
- initializeServices(config);
1600
- }
1601
- try {
1602
- let todos;
1603
- if (todayOnly) {
1604
- todos = await todoListManager.getTodaysTasks();
1605
- }
1606
- else {
1607
- todos = await todoListManager.listTodos({
1608
- priority: priority,
1609
- status,
1610
- source,
1611
- tags,
1612
- });
1613
- }
1614
- // Format todos for display
1615
- const formattedTodos = todos.map((todo) => ({
1616
- id: todo.id,
1617
- title: todo.title,
1618
- priority: todo.priority,
1619
- status: todo.status,
1620
- dueDate: todo.dueDate,
1621
- source: todo.source,
1622
- tags: todo.tags,
1623
- estimatedMinutes: todo.estimatedMinutes,
1624
- stakeholders: todo.stakeholders,
1625
- }));
1626
- return {
1627
- content: [
1628
- {
1629
- type: "text",
1630
- text: JSON.stringify({
1631
- success: true,
1632
- totalCount: todos.length,
1633
- todos: formattedTodos,
1634
- message: todos.length > 0
1635
- ? `${todos.length}件のタスクが見つかりました。`
1636
- : "タスクが見つかりませんでした。",
1637
- filters: {
1638
- priority,
1639
- status,
1640
- source,
1641
- todayOnly,
1642
- tags,
1643
- },
1644
- }, null, 2),
1645
- },
1646
- ],
1647
- };
1648
- }
1649
- catch (error) {
1650
- return {
1651
- content: [
1652
- {
1653
- type: "text",
1654
- text: JSON.stringify({
1655
- error: true,
1656
- message: `TODOリストの取得に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1657
- }, null, 2),
1658
- },
1659
- ],
1660
- };
1661
- }
1662
- });
1663
- /**
1664
- * update_task_status - Update the status of a task
1665
- * Requirement: 12.5, 12.6
1666
- */
424
+ }, async ({ priority, status, source, todayOnly, tags }) => handleListTodos(createReminderTodoContext(), {
425
+ priority,
426
+ status,
427
+ source,
428
+ todayOnly,
429
+ tags,
430
+ }));
431
+ // update_task_status - uses extracted handler
1667
432
  server.tool("update_task_status", "Update the status of a task in Apple Reminders or Notion.", {
1668
433
  taskId: z.string().describe("ID of the task to update"),
1669
434
  status: z
@@ -1676,7 +441,35 @@ async function createServer() {
1676
441
  .boolean()
1677
442
  .optional()
1678
443
  .describe("Whether to sync the status across all sources"),
1679
- }, async ({ taskId, status, source, syncAcrossSources }) => {
444
+ }, async ({ taskId, status, source, syncAcrossSources }) => handleUpdateTaskStatus(createTaskToolsContext(), {
445
+ taskId,
446
+ status,
447
+ source,
448
+ syncAcrossSources,
449
+ }));
450
+ // sync_tasks - uses extracted handler
451
+ server.tool("sync_tasks", "Synchronize tasks between Apple Reminders and Notion, detecting and resolving conflicts.", {}, async () => handleSyncTasks(createTaskToolsContext()));
452
+ // detect_duplicates - uses extracted handler
453
+ server.tool("detect_duplicates", "Detect duplicate tasks between Apple Reminders and Notion.", {
454
+ autoMerge: z
455
+ .boolean()
456
+ .optional()
457
+ .describe("Whether to automatically merge high-confidence duplicates"),
458
+ }, async ({ autoMerge }) => handleDetectDuplicates(createTaskToolsContext(), { autoMerge }));
459
+ // list_calendar_sources - uses extracted handler
460
+ server.tool("list_calendar_sources", "List available and enabled calendar sources (EventKit, Google Calendar) with their health status. Shows which sources can be used and their current state.", {}, async () => handleListCalendarSources(createCalendarToolsContext()));
461
+ /**
462
+ * set_calendar_source - Enable or disable a calendar source
463
+ * Requirement: 9, 11, Task 33
464
+ */
465
+ server.tool("set_calendar_source", "Enable or disable a calendar source (EventKit or Google Calendar). When enabling Google Calendar for the first time, this will initiate the OAuth flow. Returns authorization URL if OAuth is required.", {
466
+ source: z
467
+ .enum(['eventkit', 'google'])
468
+ .describe("Calendar source to configure: 'eventkit' (macOS only) or 'google' (all platforms)"),
469
+ enabled: z
470
+ .boolean()
471
+ .describe("Whether to enable (true) or disable (false) the source"),
472
+ }, async ({ source, enabled }) => {
1680
473
  if (!config) {
1681
474
  return {
1682
475
  content: [
@@ -1690,68 +483,152 @@ async function createServer() {
1690
483
  ],
1691
484
  };
1692
485
  }
1693
- if (!todoListManager) {
486
+ if (!calendarSourceManager) {
1694
487
  initializeServices(config);
1695
488
  }
1696
489
  try {
1697
- // Update the task status
1698
- const result = await todoListManager.updateTaskStatus(taskId, status, source);
1699
- if (!result.success) {
490
+ // Check if source is available on this platform
491
+ const availableSources = await calendarSourceManager.detectAvailableSources();
492
+ if (source === 'eventkit' && !availableSources.eventkit) {
1700
493
  return {
1701
494
  content: [
1702
495
  {
1703
496
  type: "text",
1704
497
  text: JSON.stringify({
1705
498
  success: false,
1706
- taskId,
1707
- error: result.error,
1708
- message: `タスクステータスの更新に失敗しました: ${result.error}`,
499
+ message: "EventKitはこのプラットフォームでは利用できません。EventKitはmacOSでのみ利用可能です。",
500
+ }, null, 2),
501
+ },
502
+ ],
503
+ };
504
+ }
505
+ if (enabled) {
506
+ // Enable the source
507
+ await calendarSourceManager.enableSource(source);
508
+ // If enabling Google Calendar for the first time, check if OAuth is needed
509
+ if (source === 'google' && googleCalendarService) {
510
+ try {
511
+ // Check if tokens already exist
512
+ const { GoogleOAuthHandler } = await import('./oauth/google-oauth-handler.js');
513
+ const oauthConfig = {
514
+ clientId: process.env.GOOGLE_CLIENT_ID || '',
515
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET || '',
516
+ redirectUri: process.env.GOOGLE_REDIRECT_URI || 'http://localhost:3000/oauth/callback',
517
+ };
518
+ if (!oauthConfig.clientId || !oauthConfig.clientSecret) {
519
+ return {
520
+ content: [
521
+ {
522
+ type: "text",
523
+ text: JSON.stringify({
524
+ success: false,
525
+ message: "Google Calendar OAuth設定が見つかりません。環境変数GOOGLE_CLIENT_IDとGOOGLE_CLIENT_SECRETを設定してください。",
526
+ requiredEnvVars: [
527
+ 'GOOGLE_CLIENT_ID',
528
+ 'GOOGLE_CLIENT_SECRET',
529
+ 'GOOGLE_REDIRECT_URI (optional, defaults to http://localhost:3000/oauth/callback)',
530
+ ],
531
+ }, null, 2),
532
+ },
533
+ ],
534
+ };
535
+ }
536
+ const oauthHandler = new GoogleOAuthHandler(oauthConfig);
537
+ // Try to get existing tokens
538
+ const existingTokens = await oauthHandler.getTokens();
539
+ if (!existingTokens) {
540
+ // Need to initiate OAuth flow
541
+ const authUrl = await oauthHandler.getAuthorizationUrl();
542
+ // Save config before OAuth flow
543
+ await ConfigLoader.save(config);
544
+ return {
545
+ content: [
546
+ {
547
+ type: "text",
548
+ text: JSON.stringify({
549
+ success: true,
550
+ source,
551
+ enabled: true,
552
+ oauthRequired: true,
553
+ authorizationUrl: authUrl,
554
+ message: `Google Calendarを有効化しました。OAuth認証が必要です。以下のURLにアクセスして認証を完了してください: ${authUrl}`,
555
+ instructions: [
556
+ '1. 上記のURLをブラウザで開く',
557
+ '2. Googleアカウントでログイン',
558
+ '3. sage アプリケーションにカレンダーへのアクセスを許可',
559
+ '4. リダイレクトされたURLから認証コードを取得',
560
+ '5. 認証コードを使用してトークンを取得(別途実装予定)',
561
+ ],
562
+ }, null, 2),
563
+ },
564
+ ],
565
+ };
566
+ }
567
+ }
568
+ catch (error) {
569
+ // OAuth check failed, but source is enabled in config
570
+ await ConfigLoader.save(config);
571
+ return {
572
+ content: [
573
+ {
574
+ type: "text",
575
+ text: JSON.stringify({
576
+ success: true,
577
+ source,
578
+ enabled: true,
579
+ warning: `Google Calendarを有効化しましたが、OAuth設定の確認に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
580
+ message: "設定は保存されましたが、OAuth認証が必要な場合があります。",
581
+ }, null, 2),
582
+ },
583
+ ],
584
+ };
585
+ }
586
+ }
587
+ // Save the updated config
588
+ await ConfigLoader.save(config);
589
+ return {
590
+ content: [
591
+ {
592
+ type: "text",
593
+ text: JSON.stringify({
594
+ success: true,
595
+ source,
596
+ enabled: true,
597
+ message: `${source === 'eventkit' ? 'EventKit' : 'Google Calendar'}を有効化しました。`,
1709
598
  }, null, 2),
1710
599
  },
1711
600
  ],
1712
601
  };
1713
602
  }
1714
- // Optionally sync across sources
1715
- let syncResult;
1716
- if (syncAcrossSources) {
1717
- syncResult = await todoListManager.syncTaskAcrossSources(taskId);
603
+ else {
604
+ // Disable the source
605
+ await calendarSourceManager.disableSource(source);
606
+ // Save the updated config
607
+ await ConfigLoader.save(config);
608
+ return {
609
+ content: [
610
+ {
611
+ type: "text",
612
+ text: JSON.stringify({
613
+ success: true,
614
+ source,
615
+ enabled: false,
616
+ message: `${source === 'eventkit' ? 'EventKit' : 'Google Calendar'}を無効化しました。`,
617
+ }, null, 2),
618
+ },
619
+ ],
620
+ };
1718
621
  }
1719
- return {
1720
- content: [
1721
- {
1722
- type: "text",
1723
- text: JSON.stringify({
1724
- success: true,
1725
- taskId,
1726
- newStatus: status,
1727
- updatedFields: result.updatedFields,
1728
- syncedSources: result.syncedSources,
1729
- syncResult: syncAcrossSources ? syncResult : undefined,
1730
- message: `タスクのステータスを「${status}」に更新しました。`,
1731
- }, null, 2),
1732
- },
1733
- ],
1734
- };
1735
622
  }
1736
623
  catch (error) {
1737
- return {
1738
- content: [
1739
- {
1740
- type: "text",
1741
- text: JSON.stringify({
1742
- error: true,
1743
- message: `タスクステータスの更新に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1744
- }, null, 2),
1745
- },
1746
- ],
1747
- };
624
+ return createErrorFromCatch('カレンダーソース設定に失敗しました', error);
1748
625
  }
1749
626
  });
1750
627
  /**
1751
- * sync_tasks - Sync tasks across all sources
1752
- * Requirement: 12.6
628
+ * sync_calendar_sources - Sync events between EventKit and Google Calendar
629
+ * Requirement: 8, Task 34
1753
630
  */
1754
- server.tool("sync_tasks", "Synchronize tasks between Apple Reminders and Notion, detecting and resolving conflicts.", {}, async () => {
631
+ server.tool("sync_calendar_sources", "Synchronize calendar events between EventKit and Google Calendar. Both sources must be enabled for sync to work. Returns the number of events added, updated, and deleted.", {}, async () => {
1755
632
  if (!config) {
1756
633
  return {
1757
634
  content: [
@@ -1765,54 +642,73 @@ async function createServer() {
1765
642
  ],
1766
643
  };
1767
644
  }
1768
- if (!taskSynchronizer) {
645
+ if (!calendarSourceManager) {
1769
646
  initializeServices(config);
1770
647
  }
1771
648
  try {
1772
- const result = await taskSynchronizer.syncAllTasks();
649
+ // Check if both sources are enabled
650
+ const enabledSources = calendarSourceManager.getEnabledSources();
651
+ if (enabledSources.length < 2) {
652
+ return {
653
+ content: [
654
+ {
655
+ type: "text",
656
+ text: JSON.stringify({
657
+ success: false,
658
+ message: "同期を実行するには、EventKitとGoogle Calendarの両方を有効化する必要があります。現在有効なソース: " +
659
+ enabledSources.join(", "),
660
+ enabledSources,
661
+ }, null, 2),
662
+ },
663
+ ],
664
+ };
665
+ }
666
+ // Execute sync
667
+ const result = await calendarSourceManager.syncCalendars();
668
+ if (result.success) {
669
+ return {
670
+ content: [
671
+ {
672
+ type: "text",
673
+ text: JSON.stringify({
674
+ success: true,
675
+ eventsAdded: result.eventsAdded,
676
+ eventsUpdated: result.eventsUpdated,
677
+ eventsDeleted: result.eventsDeleted,
678
+ conflicts: result.conflicts,
679
+ errors: result.errors,
680
+ message: `カレンダー同期が完了しました。追加: ${result.eventsAdded}件、更新: ${result.eventsUpdated}件、削除: ${result.eventsDeleted}件`,
681
+ }, null, 2),
682
+ },
683
+ ],
684
+ };
685
+ }
1773
686
  return {
1774
687
  content: [
1775
688
  {
1776
689
  type: "text",
1777
690
  text: JSON.stringify({
1778
- success: true,
1779
- totalTasks: result.totalTasks,
1780
- syncedTasks: result.syncedTasks,
691
+ success: false,
692
+ eventsAdded: result.eventsAdded,
693
+ eventsUpdated: result.eventsUpdated,
694
+ eventsDeleted: result.eventsDeleted,
1781
695
  conflicts: result.conflicts,
1782
696
  errors: result.errors,
1783
- durationMs: result.duration,
1784
- message: result.conflicts.length > 0
1785
- ? `${result.syncedTasks}件のタスクを同期しました。${result.conflicts.length}件の競合が検出されました。`
1786
- : `${result.syncedTasks}件のタスクを同期しました。`,
697
+ message: "カレンダー同期中にエラーが発生しました。",
1787
698
  }, null, 2),
1788
699
  },
1789
700
  ],
1790
701
  };
1791
702
  }
1792
703
  catch (error) {
1793
- return {
1794
- content: [
1795
- {
1796
- type: "text",
1797
- text: JSON.stringify({
1798
- error: true,
1799
- message: `タスク同期に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1800
- }, null, 2),
1801
- },
1802
- ],
1803
- };
704
+ return createErrorFromCatch('カレンダー同期に失敗しました', error);
1804
705
  }
1805
706
  });
1806
707
  /**
1807
- * detect_duplicates - Detect duplicate tasks across sources
1808
- * Requirement: 12.5
708
+ * get_calendar_sync_status - Check sync status between calendar sources
709
+ * Requirement: 8, Task 35
1809
710
  */
1810
- server.tool("detect_duplicates", "Detect duplicate tasks between Apple Reminders and Notion.", {
1811
- autoMerge: z
1812
- .boolean()
1813
- .optional()
1814
- .describe("Whether to automatically merge high-confidence duplicates"),
1815
- }, async ({ autoMerge }) => {
711
+ server.tool("get_calendar_sync_status", "Check the synchronization status between EventKit and Google Calendar. Returns last sync time, next sync time, and source availability.", {}, async () => {
1816
712
  if (!config) {
1817
713
  return {
1818
714
  content: [
@@ -1826,72 +722,42 @@ async function createServer() {
1826
722
  ],
1827
723
  };
1828
724
  }
1829
- if (!taskSynchronizer) {
725
+ if (!calendarSourceManager) {
1830
726
  initializeServices(config);
1831
727
  }
1832
728
  try {
1833
- const duplicates = await taskSynchronizer.detectDuplicates();
1834
- // Format duplicates for display
1835
- const formattedDuplicates = duplicates.map((d) => ({
1836
- tasks: d.tasks.map((t) => ({
1837
- id: t.id,
1838
- title: t.title,
1839
- source: t.source,
1840
- status: t.status,
1841
- priority: t.priority,
1842
- })),
1843
- similarity: Math.round(d.similarity * 100),
1844
- confidence: d.confidence,
1845
- suggestedMerge: {
1846
- title: d.suggestedMerge.title,
1847
- priority: d.suggestedMerge.priority,
1848
- status: d.suggestedMerge.status,
1849
- tags: d.suggestedMerge.tags,
1850
- },
1851
- }));
1852
- // Auto-merge high-confidence duplicates if requested
1853
- let mergeResults;
1854
- if (autoMerge) {
1855
- const highConfidenceDuplicates = duplicates.filter((d) => d.confidence === "high");
1856
- if (highConfidenceDuplicates.length > 0) {
1857
- mergeResults = await taskSynchronizer.mergeDuplicates(highConfidenceDuplicates);
1858
- }
1859
- }
729
+ const status = await calendarSourceManager.getSyncStatus();
1860
730
  return {
1861
731
  content: [
1862
732
  {
1863
733
  type: "text",
1864
734
  text: JSON.stringify({
1865
- success: true,
1866
- duplicatesFound: duplicates.length,
1867
- duplicates: formattedDuplicates,
1868
- mergeResults: autoMerge ? mergeResults : undefined,
1869
- message: duplicates.length > 0
1870
- ? `${duplicates.length}件の重複タスクが検出されました。`
1871
- : "重複タスクは見つかりませんでした。",
735
+ isEnabled: status.isEnabled,
736
+ lastSyncTime: status.lastSyncTime || "未実行",
737
+ nextSyncTime: status.nextSyncTime || "N/A",
738
+ sources: {
739
+ eventkit: {
740
+ available: status.sources.eventkit.available,
741
+ lastError: status.sources.eventkit.lastError,
742
+ },
743
+ google: {
744
+ available: status.sources.google.available,
745
+ lastError: status.sources.google.lastError,
746
+ },
747
+ },
748
+ message: status.isEnabled
749
+ ? "カレンダー同期が有効です。"
750
+ : "カレンダー同期を有効にするには、EventKitとGoogle Calendarの両方を有効化してください。",
1872
751
  }, null, 2),
1873
752
  },
1874
753
  ],
1875
754
  };
1876
755
  }
1877
756
  catch (error) {
1878
- return {
1879
- content: [
1880
- {
1881
- type: "text",
1882
- text: JSON.stringify({
1883
- error: true,
1884
- message: `重複検出に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1885
- }, null, 2),
1886
- },
1887
- ],
1888
- };
757
+ return createErrorFromCatch('同期状態の取得に失敗しました', error);
1889
758
  }
1890
759
  });
1891
- /**
1892
- * get_working_cadence - Get user's working rhythm information
1893
- * Requirement: 32.1-32.10
1894
- */
760
+ // get_working_cadence - uses extracted handler
1895
761
  server.tool("get_working_cadence", "Get user's working rhythm including deep work days, meeting-heavy days, and scheduling recommendations.", {
1896
762
  dayOfWeek: z
1897
763
  .enum([
@@ -1909,62 +775,7 @@ async function createServer() {
1909
775
  .string()
1910
776
  .optional()
1911
777
  .describe("Get info for a specific date in ISO 8601 format (e.g., 2025-01-15)"),
1912
- }, async ({ dayOfWeek, date }) => {
1913
- // Initialize service if not already done
1914
- if (!workingCadenceService) {
1915
- workingCadenceService = new WorkingCadenceService();
1916
- }
1917
- try {
1918
- const result = await workingCadenceService.getWorkingCadence({
1919
- dayOfWeek,
1920
- date,
1921
- });
1922
- if (!result.success) {
1923
- return {
1924
- content: [
1925
- {
1926
- type: "text",
1927
- text: JSON.stringify({
1928
- error: true,
1929
- message: result.error || "勤務リズム情報の取得に失敗しました。",
1930
- }, null, 2),
1931
- },
1932
- ],
1933
- };
1934
- }
1935
- return {
1936
- content: [
1937
- {
1938
- type: "text",
1939
- text: JSON.stringify({
1940
- success: true,
1941
- user: result.user,
1942
- workingHours: result.workingHours,
1943
- weeklyPattern: result.weeklyPattern,
1944
- deepWorkBlocks: result.deepWorkBlocks,
1945
- weeklyReview: result.weeklyReview,
1946
- specificDay: result.specificDay,
1947
- recommendations: result.recommendations,
1948
- summary: result.summary,
1949
- }, null, 2),
1950
- },
1951
- ],
1952
- };
1953
- }
1954
- catch (error) {
1955
- return {
1956
- content: [
1957
- {
1958
- type: "text",
1959
- text: JSON.stringify({
1960
- error: true,
1961
- message: `勤務リズム情報の取得に失敗しました: ${error instanceof Error ? error.message : "Unknown error"}`,
1962
- }, null, 2),
1963
- },
1964
- ],
1965
- };
1966
- }
1967
- });
778
+ }, async ({ dayOfWeek, date }) => handleGetWorkingCadence(createCalendarToolsContext(), { dayOfWeek, date }));
1968
779
  return server;
1969
780
  }
1970
781
  /**