@imenam/mcp-organizer 0.0.0-stage → 1.0.2

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 (165) hide show
  1. package/README.md +548 -3
  2. package/dist/shared/logic/first-account.d.ts +13 -0
  3. package/dist/shared/logic/first-account.js +15 -0
  4. package/dist/shared/logic/labels.d.ts +5 -0
  5. package/dist/shared/logic/labels.js +17 -0
  6. package/dist/shared/logic/positions.d.ts +9 -0
  7. package/dist/shared/logic/positions.js +19 -0
  8. package/dist/shared/logic/spaces.d.ts +13 -0
  9. package/dist/shared/logic/spaces.js +48 -0
  10. package/dist/shared/logic/test-summary.d.ts +28 -0
  11. package/dist/shared/logic/test-summary.js +35 -0
  12. package/dist/shared/schemas/agent-api.schema.d.ts +269 -0
  13. package/dist/shared/schemas/agent-api.schema.js +97 -0
  14. package/dist/shared/schemas/agent-tools.schema.d.ts +818 -0
  15. package/dist/shared/schemas/agent-tools.schema.js +198 -0
  16. package/dist/shared/schemas/api.schema.d.ts +249 -0
  17. package/dist/shared/schemas/api.schema.js +116 -0
  18. package/dist/shared/schemas/entities.schema.d.ts +472 -0
  19. package/dist/shared/schemas/entities.schema.js +262 -0
  20. package/dist/shared/schemas/enums.schema.d.ts +135 -0
  21. package/dist/shared/schemas/enums.schema.js +58 -0
  22. package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
  23. package/dist/shared/schemas/gui-api.schema.js +20 -0
  24. package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
  25. package/dist/shared/schemas/ideas-api.schema.js +40 -0
  26. package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
  27. package/dist/shared/schemas/notifications-api.schema.js +58 -0
  28. package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
  29. package/dist/shared/schemas/tasks-api.schema.js +225 -0
  30. package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
  31. package/dist/shared/schemas/tokens-api.schema.js +42 -0
  32. package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
  33. package/dist/shared/schemas/transcription-api.schema.js +12 -0
  34. package/dist/src/agent-api/checklist.d.ts +3 -0
  35. package/dist/src/agent-api/checklist.js +127 -0
  36. package/dist/src/agent-api/ideas.d.ts +3 -0
  37. package/dist/src/agent-api/ideas.js +93 -0
  38. package/dist/src/agent-api/notifications.d.ts +16 -0
  39. package/dist/src/agent-api/notifications.js +91 -0
  40. package/dist/src/agent-api/requests.d.ts +3 -0
  41. package/dist/src/agent-api/requests.js +60 -0
  42. package/dist/src/agent-api/router.d.ts +3 -0
  43. package/dist/src/agent-api/router.js +41 -0
  44. package/dist/src/agent-api/tasks.d.ts +2 -0
  45. package/dist/src/agent-api/tasks.js +290 -0
  46. package/dist/src/agent-api/types.d.ts +41 -0
  47. package/dist/src/agent-api/types.js +14 -0
  48. package/dist/src/cli/backup.d.ts +12 -0
  49. package/dist/src/cli/backup.js +126 -0
  50. package/dist/src/cli/prompt.d.ts +7 -0
  51. package/dist/src/cli/prompt.js +40 -0
  52. package/dist/src/cli/set-user.d.ts +2 -0
  53. package/dist/src/cli/set-user.js +114 -0
  54. package/dist/src/cli/setup-mcp.d.ts +10 -0
  55. package/dist/src/cli/setup-mcp.js +110 -0
  56. package/dist/src/cli/setup.d.ts +39 -0
  57. package/dist/src/cli/setup.js +626 -0
  58. package/dist/src/cli/watch.d.ts +4 -0
  59. package/dist/src/cli/watch.js +62 -0
  60. package/dist/src/core/accounts.d.ts +129 -0
  61. package/dist/src/core/accounts.js +284 -0
  62. package/dist/src/core/agent-tokens.d.ts +31 -0
  63. package/dist/src/core/agent-tokens.js +120 -0
  64. package/dist/src/core/agent.d.ts +71 -0
  65. package/dist/src/core/agent.js +358 -0
  66. package/dist/src/core/attachments.d.ts +21 -0
  67. package/dist/src/core/attachments.js +44 -0
  68. package/dist/src/core/data-dir.d.ts +5 -0
  69. package/dist/src/core/data-dir.js +11 -0
  70. package/dist/src/core/db.d.ts +13 -0
  71. package/dist/src/core/db.js +38 -0
  72. package/dist/src/core/env-file.d.ts +11 -0
  73. package/dist/src/core/env-file.js +36 -0
  74. package/dist/src/core/errors.d.ts +24 -0
  75. package/dist/src/core/errors.js +42 -0
  76. package/dist/src/core/history.d.ts +34 -0
  77. package/dist/src/core/history.js +279 -0
  78. package/dist/src/core/ideas.d.ts +16 -0
  79. package/dist/src/core/ideas.js +100 -0
  80. package/dist/src/core/ids.d.ts +2 -0
  81. package/dist/src/core/ids.js +5 -0
  82. package/dist/src/core/log-dir.d.ts +2 -0
  83. package/dist/src/core/log-dir.js +9 -0
  84. package/dist/src/core/logger.d.ts +14 -0
  85. package/dist/src/core/logger.js +37 -0
  86. package/dist/src/core/mail.d.ts +43 -0
  87. package/dist/src/core/mail.js +44 -0
  88. package/dist/src/core/mcp-entry.d.ts +20 -0
  89. package/dist/src/core/mcp-entry.js +15 -0
  90. package/dist/src/core/migrations.d.ts +18 -0
  91. package/dist/src/core/migrations.js +404 -0
  92. package/dist/src/core/native-sqlite.d.ts +2 -0
  93. package/dist/src/core/native-sqlite.js +23 -0
  94. package/dist/src/core/notifications.d.ts +46 -0
  95. package/dist/src/core/notifications.js +173 -0
  96. package/dist/src/core/project-root.d.ts +2 -0
  97. package/dist/src/core/project-root.js +13 -0
  98. package/dist/src/core/requests.d.ts +50 -0
  99. package/dist/src/core/requests.js +240 -0
  100. package/dist/src/core/snapshots.d.ts +32 -0
  101. package/dist/src/core/snapshots.js +35 -0
  102. package/dist/src/core/spaces.d.ts +66 -0
  103. package/dist/src/core/spaces.js +252 -0
  104. package/dist/src/core/store.d.ts +577 -0
  105. package/dist/src/core/store.js +2119 -0
  106. package/dist/src/core/task-history.d.ts +7 -0
  107. package/dist/src/core/task-history.js +41 -0
  108. package/dist/src/core/tasks.d.ts +58 -0
  109. package/dist/src/core/tasks.js +389 -0
  110. package/dist/src/core/test-tasks.d.ts +19 -0
  111. package/dist/src/core/test-tasks.js +135 -0
  112. package/dist/src/core/transcription.d.ts +45 -0
  113. package/dist/src/core/transcription.js +92 -0
  114. package/dist/src/core/watchers.d.ts +30 -0
  115. package/dist/src/core/watchers.js +50 -0
  116. package/dist/src/deploy/env-render.d.ts +26 -0
  117. package/dist/src/deploy/env-render.js +98 -0
  118. package/dist/src/deploy/templates.d.ts +38 -0
  119. package/dist/src/deploy/templates.js +130 -0
  120. package/dist/src/gui-api.d.ts +37 -0
  121. package/dist/src/gui-api.js +383 -0
  122. package/dist/src/index.d.ts +2 -0
  123. package/dist/src/index.js +110 -0
  124. package/dist/src/mcp/client.d.ts +38 -0
  125. package/dist/src/mcp/client.js +106 -0
  126. package/dist/src/mcp/notifications.d.ts +13 -0
  127. package/dist/src/mcp/notifications.js +54 -0
  128. package/dist/src/mcp/start.d.ts +14 -0
  129. package/dist/src/mcp/start.js +123 -0
  130. package/dist/src/mcp/tools.d.ts +46 -0
  131. package/dist/src/mcp/tools.js +348 -0
  132. package/dist/src/mcp/watch-handoff.d.ts +35 -0
  133. package/dist/src/mcp/watch-handoff.js +91 -0
  134. package/dist/src/server/app.d.ts +25 -0
  135. package/dist/src/server/app.js +305 -0
  136. package/dist/src/server/config.d.ts +23 -0
  137. package/dist/src/server/config.js +75 -0
  138. package/dist/src/server/limits.d.ts +2 -0
  139. package/dist/src/server/limits.js +4 -0
  140. package/dist/src/server/login-limiter.d.ts +36 -0
  141. package/dist/src/server/login-limiter.js +81 -0
  142. package/dist/src/server/start.d.ts +5 -0
  143. package/dist/src/server/start.js +88 -0
  144. package/dist/src/version.d.ts +1 -0
  145. package/dist/src/version.js +16 -0
  146. package/gui/dist/assets/index-BVN0agCc.css +1 -0
  147. package/gui/dist/assets/index-DI1_kr81.js +70 -0
  148. package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  149. package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  150. package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  151. package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  152. package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  153. package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  154. package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  155. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  156. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  157. package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  158. package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  159. package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  160. package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  161. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  162. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  163. package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  164. package/gui/dist/index.html +13 -0
  165. package/package.json +66 -4
@@ -0,0 +1,383 @@
1
+ import { ActivateRequestSchema, ChangePasswordRequestSchema, CreateSpaceRequestSchema, InviteRequestSchema, LoginRequestSchema, RenameSpaceRequestSchema, UpdateProfileRequestSchema, } from "../shared/schemas/api.schema.js";
2
+ import { schemaVersion } from "./core/migrations.js";
3
+ import { ForbiddenError, InvariantError, NotFoundError, OrganizerError, UnauthorizedError } from "./core/errors.js";
4
+ import { authenticate, changePassword, login, logout, redeemAccessLink, resolveAccessLink, updateDisplayName } from "./core/accounts.js";
5
+ import { acceptReceivedInvitation, createSharedSpace, declineReceivedInvitation, deleteSpace, inviteToSpace, leaveSpace, listMembers, listReceivedInvitations, listSpaces, removeMember, renameSpace, resendInvitation, revokeInvitation, } from "./core/spaces.js";
6
+ import { addChecklistItem, addComment, createTask, deleteAttachment, deleteChecklistItem, deleteComment, deleteTask, downloadAttachment, getBoard, getTaskDetail, listChanges, moveChecklistItem, moveTask, setAssignees, setTags, updateChecklistItem, updateComment, updateTask, uploadAttachment, } from "./core/tasks.js";
7
+ import { addCheckpoint, deleteCheckpoint, moveCheckpoint, saveReviewDraft, setTestResult, submitReview, updateCheckpoint } from "./core/test-tasks.js";
8
+ import { AddChecklistItemRequestSchema, CheckpointTextRequestSchema, CommentRequestSchema, CreateTaskRequestSchema, MoveAfterRequestSchema, MoveTaskRequestSchema, ReviewDraftRequestSchema, SetAssigneesRequestSchema, SetTagsRequestSchema, SetTestResultRequestSchema, SubmitReviewRequestSchema, TaskFiltersSchema, UpdateChecklistItemRequestSchema, UpdateTaskRequestSchema, UploadAttachmentRequestSchema, } from "../shared/schemas/tasks-api.schema.js";
9
+ import { getSettings, listNotifications, markAllNotificationsRead, markNotificationRead, unreadCount, updateSettings } from "./core/notifications.js";
10
+ import { NotificationListQuerySchema, UpdateSettingsRequestSchema } from "../shared/schemas/notifications-api.schema.js";
11
+ import { cancelRequest, createRequest, getRequest, listRequests, releaseRequest, undoAction, undoRequest, updateRequest } from "./core/requests.js";
12
+ import { listTaskVersions, restoreTaskVersion } from "./core/task-history.js";
13
+ import { CreateRequestSchema, UpdateRequestSchema } from "../shared/schemas/agent-api.schema.js";
14
+ import { createIdea, delegatePromotion, listIdeas, promoteIdea, setIdeaArchived } from "./core/ideas.js";
15
+ import { CreateIdeaRequestSchema, PromoteIdeaRequestSchema, UpdateIdeaRequestSchema } from "../shared/schemas/ideas-api.schema.js";
16
+ import { createAgentToken, listAgentTokens, revokeAgentToken } from "./core/agent-tokens.js";
17
+ import { CreateTokenRequestSchema } from "../shared/schemas/tokens-api.schema.js";
18
+ import { TranscriptionError } from "./core/transcription.js";
19
+ import { MAX_TRANSCRIPTION_BYTES, TranscribeRequestSchema } from "../shared/schemas/transcription-api.schema.js";
20
+ /** Erreur d'une requête de la GUI, avec la nature qui fixe son code HTTP. */
21
+ export class ApiError extends Error {
22
+ kind;
23
+ status;
24
+ constructor(kind, message,
25
+ /** Code HTTP imposé à la réponse, à la place de celui de `kind`. */
26
+ status) {
27
+ super(message);
28
+ this.kind = kind;
29
+ this.status = status;
30
+ this.name = "ApiError";
31
+ }
32
+ }
33
+ /** Nature d'une erreur levée en traitant une requête de la GUI. */
34
+ export function errorKindOf(err) {
35
+ if (err instanceof ApiError)
36
+ return err.kind;
37
+ if (err instanceof TranscriptionError)
38
+ return err.status === 503 ? "unavailable" : "upstream";
39
+ if (err instanceof UnauthorizedError)
40
+ return "unauthorized";
41
+ if (err instanceof ForbiddenError)
42
+ return "forbidden";
43
+ if (err instanceof NotFoundError)
44
+ return "not_found";
45
+ if (err instanceof InvariantError)
46
+ return "conflict";
47
+ if (err instanceof OrganizerError)
48
+ return "invalid";
49
+ return "internal";
50
+ }
51
+ /** Code HTTP imposé par une erreur : celui que Groq a renvoyé pour une transcription refusée. */
52
+ export function errorStatusOf(err) {
53
+ if (err instanceof ApiError)
54
+ return err.status;
55
+ if (err instanceof TranscriptionError)
56
+ return err.status;
57
+ return undefined;
58
+ }
59
+ /** Compile `/spaces/:spaceId/members` en expression régulière et noms de paramètres. */
60
+ function compile(path) {
61
+ const keys = [];
62
+ const source = path.replace(/:([A-Za-z]+)/g, (_, key) => {
63
+ keys.push(key);
64
+ return "([^/]+)";
65
+ });
66
+ return { pattern: new RegExp(`^${source}$`), keys };
67
+ }
68
+ const routes = [];
69
+ function publicRoute(method, path, handler) {
70
+ routes.push({ method, ...compile(path), public: handler });
71
+ }
72
+ function route(method, path, handler) {
73
+ routes.push({ method, ...compile(path), handler });
74
+ }
75
+ /** Segment de chemin décodé ; un encodage `%` invalide est une erreur 400. */
76
+ function decodeSegment(raw) {
77
+ try {
78
+ return decodeURIComponent(raw);
79
+ }
80
+ catch {
81
+ throw new ApiError("invalid", `Adresse invalide : un segment du chemin est mal encodé (${raw}).`);
82
+ }
83
+ }
84
+ /** Corps validé par `schema` ; une donnée invalide est une erreur 400 qui liste les problèmes. */
85
+ function parseBody(schema, body) {
86
+ const parsed = schema.safeParse(body ?? {});
87
+ if (!parsed.success)
88
+ throw new ApiError("invalid", parsed.error.issues.map((issue) => issue.message).join(" "));
89
+ return parsed.data;
90
+ }
91
+ function publicUser(user) {
92
+ return { id: user.id, email: user.email, displayName: user.displayName, status: user.status };
93
+ }
94
+ const ok = { ok: true };
95
+ /** Taille lisible d'un nombre d'octets, en Mo. */
96
+ function megabytes(bytes) {
97
+ return `${(bytes / (1024 * 1024)).toFixed(1).replace(".", ",")} Mo`;
98
+ }
99
+ function now(ctx) {
100
+ return ctx.now?.() ?? new Date();
101
+ }
102
+ // --- Routes publiques -------------------------------------------------------------
103
+ publicRoute("GET", "/health", (ctx) => {
104
+ const health = {
105
+ status: "ok",
106
+ version: ctx.version,
107
+ schemaVersion: schemaVersion(ctx.store.db),
108
+ accountsExist: ctx.store.countUsers() > 0,
109
+ };
110
+ return { body: health };
111
+ });
112
+ publicRoute("POST", "/auth/login", async (ctx, request) => {
113
+ const input = parseBody(LoginRequestSchema, request.body);
114
+ const { user, session } = await login(ctx.store, input.email, input.password, now(ctx));
115
+ const body = { user: publicUser(user) };
116
+ return { body, session };
117
+ });
118
+ publicRoute("POST", "/auth/logout", (ctx, request) => {
119
+ logout(ctx.store, request.sessionToken);
120
+ return { body: ok, session: null };
121
+ });
122
+ publicRoute("GET", "/access-links/:token", (ctx, _request, params) => {
123
+ const { link, user } = resolveAccessLink(ctx.store, params.token, now(ctx));
124
+ const body = { email: user.email, purpose: link.purpose, displayName: user.displayName, expiresAt: link.expiresAt };
125
+ return { body };
126
+ });
127
+ publicRoute("POST", "/access-links/:token", async (ctx, request, params) => {
128
+ const input = parseBody(ActivateRequestSchema, request.body);
129
+ const { user, session } = await redeemAccessLink(ctx.store, params.token, input, now(ctx));
130
+ const body = { user: publicUser(user) };
131
+ return { body, session };
132
+ });
133
+ // --- Routes authentifiées ---------------------------------------------------------
134
+ route("GET", "/auth/me", (_ctx, { user }) => {
135
+ const body = { user: publicUser(user) };
136
+ return { body };
137
+ });
138
+ route("PATCH", "/profile", (ctx, { user, request }) => {
139
+ const input = parseBody(UpdateProfileRequestSchema, request.body);
140
+ const body = { user: publicUser(updateDisplayName(ctx.store, user.id, input.displayName)) };
141
+ return { body };
142
+ });
143
+ route("POST", "/profile/password", async (ctx, { user, request }) => {
144
+ await changePassword(ctx.store, user.id, parseBody(ChangePasswordRequestSchema, request.body), request.sessionToken);
145
+ return { body: ok };
146
+ });
147
+ route("GET", "/spaces", (ctx, { user }) => {
148
+ const body = { spaces: listSpaces(ctx.store, user.id) };
149
+ return { body };
150
+ });
151
+ route("POST", "/spaces", (ctx, { user, request }) => {
152
+ const input = parseBody(CreateSpaceRequestSchema, request.body);
153
+ return { body: createSharedSpace(ctx.store, user.id, input) };
154
+ });
155
+ route("PATCH", "/spaces/:spaceId", (ctx, { user, request, params }) => {
156
+ const input = parseBody(RenameSpaceRequestSchema, request.body);
157
+ return { body: renameSpace(ctx.store, user.id, params.spaceId, input.name) };
158
+ });
159
+ route("DELETE", "/spaces/:spaceId", (ctx, { user, params }) => {
160
+ deleteSpace(ctx.store, ctx.attachments, user.id, params.spaceId);
161
+ return { body: ok };
162
+ });
163
+ route("GET", "/spaces/:spaceId/members", (ctx, { user, params }) => ({ body: listMembers(ctx.store, user.id, params.spaceId, now(ctx)) }));
164
+ route("DELETE", "/spaces/:spaceId/members/:userId", (ctx, { user, params }) => {
165
+ removeMember(ctx.store, user.id, params.spaceId, params.userId);
166
+ return { body: ok };
167
+ });
168
+ route("POST", "/spaces/:spaceId/leave", (ctx, { user, params }) => {
169
+ leaveSpace(ctx.store, user.id, params.spaceId);
170
+ return { body: ok };
171
+ });
172
+ route("POST", "/spaces/:spaceId/invitations", async (ctx, { user, request, params }) => {
173
+ const input = parseBody(InviteRequestSchema, request.body);
174
+ return { body: await inviteToSpace(ctx.store, ctx, user.id, params.spaceId, input.email) };
175
+ });
176
+ route("POST", "/spaces/:spaceId/invitations/:invitationId/resend", async (ctx, { user, params }) => ({
177
+ body: await resendInvitation(ctx.store, ctx, user.id, params.spaceId, params.invitationId),
178
+ }));
179
+ route("DELETE", "/spaces/:spaceId/invitations/:invitationId", (ctx, { user, params }) => {
180
+ revokeInvitation(ctx.store, user.id, params.spaceId, params.invitationId);
181
+ return { body: ok };
182
+ });
183
+ route("GET", "/invitations", (ctx, { user }) => ({ body: listReceivedInvitations(ctx.store, user.id, now(ctx)) }));
184
+ route("POST", "/invitations/:invitationId/accept", (ctx, { user, params }) => ({
185
+ body: acceptReceivedInvitation(ctx.store, user.id, params.invitationId, now(ctx)),
186
+ }));
187
+ route("POST", "/invitations/:invitationId/decline", (ctx, { user, params }) => {
188
+ declineReceivedInvitation(ctx.store, user.id, params.invitationId);
189
+ return { body: ok };
190
+ });
191
+ // --- Tâches -----------------------------------------------------------------------
192
+ route("GET", "/spaces/:spaceId/tasks", (ctx, { user, request, params }) => {
193
+ const parsed = TaskFiltersSchema.safeParse(request.query);
194
+ if (!parsed.success)
195
+ throw new ApiError("invalid", `Filtre invalide : ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join("; ")}.`);
196
+ return { body: getBoard(ctx.store, user.id, params.spaceId, parsed.data) };
197
+ });
198
+ route("POST", "/spaces/:spaceId/tasks", (ctx, { user, request, params }) => ({
199
+ body: createTask(ctx.store, user.id, params.spaceId, parseBody(CreateTaskRequestSchema, request.body)),
200
+ }));
201
+ route("GET", "/tasks/:taskId", (ctx, { user, params }) => ({ body: getTaskDetail(ctx.store, user.id, params.taskId) }));
202
+ route("PATCH", "/tasks/:taskId", (ctx, { user, request, params }) => ({
203
+ body: updateTask(ctx.store, user.id, params.taskId, parseBody(UpdateTaskRequestSchema, request.body)),
204
+ }));
205
+ route("DELETE", "/tasks/:taskId", (ctx, { user, params }) => {
206
+ deleteTask(ctx.store, ctx.attachments, user.id, params.taskId);
207
+ return { body: ok };
208
+ });
209
+ route("POST", "/tasks/:taskId/move", (ctx, { user, request, params }) => {
210
+ const input = parseBody(MoveTaskRequestSchema, request.body);
211
+ return { body: moveTask(ctx.store, user.id, params.taskId, input.status, input.afterId) };
212
+ });
213
+ route("PUT", "/tasks/:taskId/assignees", (ctx, { user, request, params }) => ({
214
+ body: setAssignees(ctx.store, user.id, params.taskId, parseBody(SetAssigneesRequestSchema, request.body).userIds),
215
+ }));
216
+ route("PUT", "/tasks/:taskId/tags", (ctx, { user, request, params }) => ({
217
+ body: setTags(ctx.store, user.id, params.taskId, parseBody(SetTagsRequestSchema, request.body).names),
218
+ }));
219
+ route("POST", "/tasks/:taskId/checklist", (ctx, { user, request, params }) => ({
220
+ body: addChecklistItem(ctx.store, user.id, params.taskId, parseBody(AddChecklistItemRequestSchema, request.body).text),
221
+ }));
222
+ route("PATCH", "/checklist/:itemId", (ctx, { user, request, params }) => ({
223
+ body: updateChecklistItem(ctx.store, user.id, params.itemId, parseBody(UpdateChecklistItemRequestSchema, request.body)),
224
+ }));
225
+ route("DELETE", "/checklist/:itemId", (ctx, { user, params }) => {
226
+ deleteChecklistItem(ctx.store, user.id, params.itemId);
227
+ return { body: ok };
228
+ });
229
+ route("POST", "/checklist/:itemId/move", (ctx, { user, request, params }) => ({
230
+ body: moveChecklistItem(ctx.store, user.id, params.itemId, parseBody(MoveAfterRequestSchema, request.body).afterId),
231
+ }));
232
+ // --- Tâches de test ---------------------------------------------------------------
233
+ route("POST", "/tasks/:taskId/checkpoints", (ctx, { user, request, params }) => ({
234
+ body: addCheckpoint(ctx.store, user.id, params.taskId, parseBody(CheckpointTextRequestSchema, request.body).text),
235
+ }));
236
+ route("PATCH", "/checkpoints/:checkpointId", (ctx, { user, request, params }) => ({
237
+ body: updateCheckpoint(ctx.store, user.id, params.checkpointId, parseBody(CheckpointTextRequestSchema, request.body).text),
238
+ }));
239
+ route("DELETE", "/checkpoints/:checkpointId", (ctx, { user, params }) => {
240
+ deleteCheckpoint(ctx.store, user.id, params.checkpointId);
241
+ return { body: ok };
242
+ });
243
+ route("POST", "/checkpoints/:checkpointId/move", (ctx, { user, request, params }) => ({
244
+ body: moveCheckpoint(ctx.store, user.id, params.checkpointId, parseBody(MoveAfterRequestSchema, request.body).afterId),
245
+ }));
246
+ route("PUT", "/checkpoints/:checkpointId/result", (ctx, { user, request, params }) => ({
247
+ body: setTestResult(ctx.store, user.id, params.checkpointId, parseBody(SetTestResultRequestSchema, request.body)),
248
+ }));
249
+ route("PUT", "/tasks/:taskId/review", (ctx, { user, request, params }) => ({
250
+ body: saveReviewDraft(ctx.store, user.id, params.taskId, parseBody(ReviewDraftRequestSchema, request.body).text),
251
+ }));
252
+ route("POST", "/tasks/:taskId/review/submit", (ctx, { user, request, params }) => ({
253
+ body: submitReview(ctx.store, user.id, params.taskId, parseBody(SubmitReviewRequestSchema, request.body).text),
254
+ }));
255
+ // --- Commentaires ------------------------------------------------------------------
256
+ route("POST", "/tasks/:taskId/comments", (ctx, { user, request, params }) => ({
257
+ body: addComment(ctx.store, user.id, params.taskId, parseBody(CommentRequestSchema, request.body).body),
258
+ }));
259
+ route("PATCH", "/comments/:commentId", (ctx, { user, request, params }) => ({
260
+ body: updateComment(ctx.store, user.id, params.commentId, parseBody(CommentRequestSchema, request.body).body),
261
+ }));
262
+ route("DELETE", "/comments/:commentId", (ctx, { user, params }) => {
263
+ deleteComment(ctx.store, user.id, params.commentId);
264
+ return { body: ok };
265
+ });
266
+ route("POST", "/tasks/:taskId/attachments", (ctx, { user, request, params }) => ({
267
+ body: uploadAttachment(ctx.store, ctx.attachments, user.id, params.taskId, parseBody(UploadAttachmentRequestSchema, request.body)),
268
+ }));
269
+ route("GET", "/attachments/:attachmentId", (ctx, { user, params }) => ({
270
+ body: null,
271
+ file: downloadAttachment(ctx.store, ctx.attachments, user.id, params.attachmentId),
272
+ }));
273
+ route("DELETE", "/attachments/:attachmentId", (ctx, { user, params }) => {
274
+ deleteAttachment(ctx.store, ctx.attachments, user.id, params.attachmentId);
275
+ return { body: ok };
276
+ });
277
+ // --- Idées -------------------------------------------------------------------------
278
+ route("GET", "/spaces/:spaceId/ideas", (ctx, { user, params }) => ({ body: listIdeas(ctx.store, user.id, params.spaceId) }));
279
+ route("POST", "/spaces/:spaceId/ideas", (ctx, { user, request, params }) => ({
280
+ body: createIdea(ctx.store, user.id, params.spaceId, parseBody(CreateIdeaRequestSchema, request.body).text),
281
+ }));
282
+ route("PATCH", "/ideas/:ideaId", (ctx, { user, request, params }) => ({
283
+ body: setIdeaArchived(ctx.store, user.id, params.ideaId, parseBody(UpdateIdeaRequestSchema, request.body).archived),
284
+ }));
285
+ route("POST", "/ideas/:ideaId/promote", (ctx, { user, request, params }) => ({
286
+ body: promoteIdea(ctx.store, user.id, params.ideaId, parseBody(PromoteIdeaRequestSchema, request.body)),
287
+ }));
288
+ route("POST", "/ideas/:ideaId/delegate", (ctx, { user, params }) => ({ body: delegatePromotion(ctx.store, user.id, params.ideaId) }));
289
+ // --- File de l'agent et historique -------------------------------------------------
290
+ route("GET", "/spaces/:spaceId/requests", (ctx, { user, params }) => ({ body: listRequests(ctx.store, user.id, params.spaceId, now(ctx)) }));
291
+ route("POST", "/spaces/:spaceId/requests", (ctx, { user, request, params }) => ({
292
+ body: createRequest(ctx.store, user.id, params.spaceId, parseBody(CreateRequestSchema, request.body)),
293
+ }));
294
+ route("GET", "/requests/:requestId", (ctx, { user, params }) => ({ body: getRequest(ctx.store, user.id, params.requestId) }));
295
+ route("PATCH", "/requests/:requestId", (ctx, { user, request, params }) => ({
296
+ body: updateRequest(ctx.store, user.id, params.requestId, parseBody(UpdateRequestSchema, request.body).text),
297
+ }));
298
+ route("POST", "/requests/:requestId/cancel", (ctx, { user, params }) => ({ body: cancelRequest(ctx.store, user.id, params.requestId) }));
299
+ route("POST", "/requests/:requestId/release", (ctx, { user, params }) => ({ body: releaseRequest(ctx.store, user.id, params.requestId) }));
300
+ route("POST", "/requests/:requestId/undo", (ctx, { user, params }) => ({ body: undoRequest(ctx.store, user.id, params.requestId) }));
301
+ route("POST", "/agent-actions/:actionId/undo", (ctx, { user, params }) => ({ body: undoAction(ctx.store, user.id, params.actionId) }));
302
+ route("GET", "/tasks/:taskId/versions", (ctx, { user, params }) => ({ body: listTaskVersions(ctx.store, user.id, params.taskId) }));
303
+ route("POST", "/tasks/:taskId/versions/:version/restore", (ctx, { user, params }) => {
304
+ const version = Number(params.version);
305
+ if (!Number.isInteger(version) || version < 1)
306
+ throw new ApiError("invalid", `Numéro de version invalide (${params.version}).`);
307
+ return { body: restoreTaskVersion(ctx.store, user.id, params.taskId, version) };
308
+ });
309
+ // --- Notifications et paramètres ---------------------------------------------------
310
+ route("GET", "/notifications", (ctx, { user, request }) => {
311
+ const parsed = NotificationListQuerySchema.safeParse(request.query);
312
+ if (!parsed.success)
313
+ throw new ApiError("invalid", `Pagination invalide : ${parsed.error.issues.map((issue) => `${issue.path.join(".")} ${issue.message}`).join("; ")}.`);
314
+ return { body: listNotifications(ctx.store, user.id, parsed.data) };
315
+ });
316
+ route("GET", "/notifications/unread-count", (ctx, { user }) => ({ body: unreadCount(ctx.store, user.id) }));
317
+ route("POST", "/notifications/read-all", (ctx, { user }) => ({ body: markAllNotificationsRead(ctx.store, user.id) }));
318
+ route("POST", "/notifications/:notificationId/read", (ctx, { user, params }) => ({
319
+ body: markNotificationRead(ctx.store, user.id, params.notificationId),
320
+ }));
321
+ route("GET", "/settings", (ctx, { user }) => ({ body: getSettings(ctx.store, ctx, user.id) }));
322
+ route("PUT", "/settings", (ctx, { user, request }) => ({
323
+ body: updateSettings(ctx.store, ctx, user.id, parseBody(UpdateSettingsRequestSchema, request.body)),
324
+ }));
325
+ // --- Jetons d'agent ---------------------------------------------------------------
326
+ route("GET", "/tokens", (ctx, { user }) => ({ body: listAgentTokens(ctx.store, user.id, now(ctx)) }));
327
+ route("POST", "/tokens", (ctx, { user, request }) => {
328
+ const input = parseBody(CreateTokenRequestSchema, request.body);
329
+ if (!ctx.publicUrl)
330
+ throw new ApiError("unavailable", "ORGANIZER_PUBLIC_URL n'est pas défini : l'URL du site, que l'agent appelle, est inconnue.");
331
+ return { body: createAgentToken(ctx.store, user.id, input, ctx.publicUrl, now(ctx)) };
332
+ });
333
+ route("POST", "/tokens/:tokenId/revoke", (ctx, { user, params }) => ({ body: revokeAgentToken(ctx.store, user.id, params.tokenId, now(ctx)) }));
334
+ // --- Dictée ------------------------------------------------------------------------
335
+ route("POST", "/transcribe", async (ctx, { request }) => {
336
+ const input = parseBody(TranscribeRequestSchema, request.body);
337
+ if (!ctx.transcriber.enabled)
338
+ throw new ApiError("unavailable", ctx.transcriber.disabledReason ?? "Dictée désactivée.");
339
+ const content = Buffer.from(input.contentBase64, "base64");
340
+ if (content.length > MAX_TRANSCRIPTION_BYTES) {
341
+ throw new ApiError("too_large", `Enregistrement trop volumineux (${megabytes(content.length)}) : la dictée accepte ${megabytes(MAX_TRANSCRIPTION_BYTES)} au plus.`);
342
+ }
343
+ if (content.length === 0)
344
+ throw new ApiError("invalid", "L'enregistrement est vide.");
345
+ const body = { text: await ctx.transcriber.transcribe({ content, filename: input.filename, mime: input.mime }) };
346
+ return { body };
347
+ });
348
+ // --- Changements -------------------------------------------------------------------
349
+ route("GET", "/changes", (ctx, { user, request }) => {
350
+ const raw = request.query.since;
351
+ let since = null;
352
+ if (raw !== undefined) {
353
+ since = Number(raw);
354
+ if (!Number.isInteger(since) || since < 0)
355
+ throw new ApiError("invalid", `Paramètre since invalide (${raw}) : entier positif attendu.`);
356
+ }
357
+ return { body: listChanges(ctx.store, user.id, since) };
358
+ });
359
+ // --- Point d'entrée ---------------------------------------------------------------
360
+ /**
361
+ * Traite une requête `/api/*` de la GUI. Hors des routes
362
+ * publiques (santé, connexion, déconnexion, liens d'accès), une session valide
363
+ * est exigée ; une session prolongée renvoie son nouveau cookie.
364
+ */
365
+ export async function handleApiRequest(ctx, request) {
366
+ for (const candidate of routes) {
367
+ if (candidate.method !== request.method)
368
+ continue;
369
+ const match = candidate.pattern.exec(request.path);
370
+ if (!match)
371
+ continue;
372
+ const params = {};
373
+ candidate.keys.forEach((key, index) => (params[key] = decodeSegment(match[index + 1])));
374
+ if (candidate.public)
375
+ return candidate.public(ctx, request, params);
376
+ const auth = authenticate(ctx.store, request.sessionToken, now(ctx));
377
+ if (!auth)
378
+ throw new ApiError("unauthorized", "Session absente ou expirée : connectez-vous.");
379
+ const response = await candidate.handler(ctx, { request, params, user: auth.user });
380
+ return auth.refreshed && response.session === undefined ? { ...response, session: auth.refreshed } : response;
381
+ }
382
+ throw new ApiError("not_found", `Unknown API route: ${request.method} /api${request.path}`);
383
+ }
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
4
+ import { APP_VERSION } from "./version.js";
5
+ import { ConfigError } from "./core/errors.js";
6
+ import { findProjectRoot } from "./core/project-root.js";
7
+ import { loadEnvFile, resolveEnvFile, withoutEnvFileFlag } from "./core/env-file.js";
8
+ const USAGE = `mcp-organizer v${APP_VERSION} — gestionnaire de tâches multi-utilisateur piloté par un agent
9
+
10
+ Usage :
11
+ mcp-organizer Mode MCP (stdio) : outils de l'agent, relayés à l'API du site.
12
+ mcp-organizer --server Site web : interface, API et base de données, sur HOST:PORT.
13
+ mcp-organizer --set-user [email] [--name="Prénom Nom"] [--non-interactive]
14
+ Crée un compte ou remplace son mot de passe.
15
+ mcp-organizer --setup [options] Installe le site : .env, unité systemd, bloc nginx, premier compte
16
+ (interactif, ou --non-interactive ; --setup --help pour les options).
17
+ mcp-organizer --backup <fichier> [--force]
18
+ Sauvegarde la base (site en service ou non) et ses pièces jointes
19
+ (dossier <fichier>.attachments).
20
+ mcp-organizer --claude-setup-mcp [--force] --url=<URL du site> --token=<jeton>
21
+ Écrit l'entrée du mode MCP dans ./.mcp.json.
22
+ mcp-organizer --watch <id du jeton>
23
+ Veilleur de notifications : une ligne par demande passée en attente.
24
+ Lancé par l'outil Monitor de l'agent, avec la commande que rend
25
+ l'outil enable_notifications.
26
+ mcp-organizer --version Affiche la version.
27
+ mcp-organizer --help Affiche cette aide.
28
+
29
+ Option commune : --dotenv=<fichier> lit ce fichier .env. Sans elle, le site et ses commandes
30
+ lisent <package>/.env ; le mode MCP n'en lit aucun.
31
+ Le site exige PORT et ORGANIZER_PUBLIC_URL ; HOST vaut 127.0.0.1 par défaut.
32
+ Le mode MCP exige ORGANIZER_URL (URL publique du site, en https hors de la machine locale)
33
+ et ORGANIZER_TOKEN (jeton d'agent), fournis par son environnement.
34
+ Voir le README pour la liste complète des variables.`;
35
+ /** Commandes qui ouvrent la base du site ; toute autre invocation démarre le mode MCP. */
36
+ const SITE_COMMANDS = ["--server", "--set-user", "--setup", "--backup"];
37
+ async function main() {
38
+ const argv = process.argv.slice(2);
39
+ if (argv.includes("--setup") && (argv.includes("--help") || argv.includes("-h"))) {
40
+ const { SETUP_USAGE } = await import("./cli/setup.js");
41
+ process.stdout.write(`${SETUP_USAGE}\n`);
42
+ return;
43
+ }
44
+ if (argv.includes("--help") || argv.includes("-h")) {
45
+ process.stdout.write(`${USAGE}\n`);
46
+ return;
47
+ }
48
+ if (argv.includes("--version") || argv.includes("-v")) {
49
+ process.stdout.write(`${APP_VERSION}\n`);
50
+ return;
51
+ }
52
+ // La commande de setup s'exécute dans le projet de l'utilisateur, avant toute lecture de configuration.
53
+ if (argv.includes("--claude-setup-mcp")) {
54
+ const { runSetupMcp } = await import("./cli/setup-mcp.js");
55
+ process.exit(runSetupMcp(argv, process.cwd()));
56
+ }
57
+ // Le veilleur lit l'URL du site et le jeton dans le relais déposé par enable_notifications.
58
+ const watchIndex = argv.indexOf("--watch");
59
+ if (watchIndex !== -1) {
60
+ const { runWatchMode } = await import("./cli/watch.js");
61
+ process.exit(await runWatchMode(argv[watchIndex + 1]));
62
+ }
63
+ const rootDir = findProjectRoot(path.dirname(fileURLToPath(import.meta.url)));
64
+ const siteCommand = SITE_COMMANDS.some((flag) => argv.includes(flag));
65
+ // Les commandes du site ouvrent la base : better-sqlite3 doit avoir son module natif.
66
+ if (siteCommand) {
67
+ const { assertSqliteLoadable } = await import("./core/native-sqlite.js");
68
+ await assertSqliteLoadable();
69
+ }
70
+ // Le setup écrit le .env : il le relit lui-même, sans le charger dans l'environnement.
71
+ if (argv.includes("--setup")) {
72
+ const { runSetup } = await import("./cli/setup.js");
73
+ process.exit(await runSetup(argv, rootDir));
74
+ }
75
+ // Le mode MCP tient sa configuration de l'environnement que lui donne l'agent ou la gateway ;
76
+ // il ne lit un fichier .env que désigné par --dotenv.
77
+ try {
78
+ const envFile = resolveEnvFile(argv, rootDir);
79
+ if (siteCommand || envFile.required)
80
+ loadEnvFile(envFile);
81
+ }
82
+ catch (err) {
83
+ throw new ConfigError(err?.message ?? String(err));
84
+ }
85
+ const args = withoutEnvFileFlag(argv);
86
+ if (args.includes("--set-user")) {
87
+ const [{ runSetUser }, { resolveDataDir }] = await Promise.all([import("./cli/set-user.js"), import("./core/data-dir.js")]);
88
+ process.exit(await runSetUser(args, resolveDataDir(rootDir)));
89
+ }
90
+ if (args.includes("--backup")) {
91
+ const [{ runBackup }, { resolveDataDir }] = await Promise.all([import("./cli/backup.js"), import("./core/data-dir.js")]);
92
+ process.exit(await runBackup(args, resolveDataDir(rootDir)));
93
+ }
94
+ if (args.includes("--server")) {
95
+ const { runServerMode } = await import("./server/start.js");
96
+ await runServerMode(rootDir);
97
+ return;
98
+ }
99
+ const { runMcpMode } = await import("./mcp/start.js");
100
+ await runMcpMode();
101
+ }
102
+ process.on("uncaughtException", (err) => {
103
+ process.stderr.write(`Uncaught exception: ${err.stack ?? err.message}\n`);
104
+ process.exit(1);
105
+ });
106
+ main().catch((err) => {
107
+ // Erreur de configuration : le message suffit, la pile d'appels n'aide personne.
108
+ process.stderr.write(err instanceof ConfigError ? `${err.message}\n` : `Fatal error: ${err?.stack ?? err}\n`);
109
+ process.exit(1);
110
+ });
@@ -0,0 +1,38 @@
1
+ import type { z } from "zod";
2
+ import { type AgentEnvelope, type WaitRequestsData } from "../../shared/schemas/agent-tools.schema.js";
3
+ import type { ToolHttpRequest } from "./tools.js";
4
+ /**
5
+ * Client HTTP de l'API agent du site : seul point de sortie réseau du mode
6
+ * MCP. Chaque appel porte le jeton dans `Authorization: Bearer`.
7
+ */
8
+ /** Délai maximal d'un appel au site, hors attente longue. */
9
+ export declare const REQUEST_TIMEOUT_MS = 30000;
10
+ /** Échec d'un appel : refus du site (avec son message) ou site injoignable. */
11
+ export declare class SiteError extends Error {
12
+ readonly status?: number | undefined;
13
+ constructor(message: string, status?: number | undefined);
14
+ }
15
+ export interface SiteConnection {
16
+ /** URL publique du site, sans barre oblique finale. */
17
+ url: string;
18
+ token: string;
19
+ }
20
+ export declare class OrganizerSiteClient {
21
+ private readonly connection;
22
+ constructor(connection: SiteConnection);
23
+ /** Exécute la requête d'un outil et renvoie le texte rédigé pour l'agent. */
24
+ call(request: ToolHttpRequest): Promise<string>;
25
+ /**
26
+ * Attente longue des demandes qui passent en attente (`GET requests/wait`) ;
27
+ * le délai de l'appel couvre la durée de l'attente.
28
+ */
29
+ waitForRequests(query: {
30
+ timeout_seconds: number;
31
+ since?: string;
32
+ watcher?: boolean;
33
+ }): Promise<WaitRequestsData>;
34
+ /** Données structurées d'une réponse, validées par `schema`. */
35
+ parseData<S extends z.ZodType>(schema: S, data: unknown): z.infer<S>;
36
+ /** Exécute une requête de l'API agent et renvoie sa réponse : texte rédigé pour l'agent et données structurées. */
37
+ envelope(request: ToolHttpRequest, timeoutMs?: number): Promise<AgentEnvelope>;
38
+ }
@@ -0,0 +1,106 @@
1
+ import { AgentEnvelopeSchema, AgentErrorSchema, WaitRequestsDataSchema } from "../../shared/schemas/agent-tools.schema.js";
2
+ /**
3
+ * Client HTTP de l'API agent du site : seul point de sortie réseau du mode
4
+ * MCP. Chaque appel porte le jeton dans `Authorization: Bearer`.
5
+ */
6
+ /** Délai maximal d'un appel au site, hors attente longue. */
7
+ export const REQUEST_TIMEOUT_MS = 30_000;
8
+ /** Échec d'un appel : refus du site (avec son message) ou site injoignable. */
9
+ export class SiteError extends Error {
10
+ status;
11
+ constructor(message, status) {
12
+ super(message);
13
+ this.status = status;
14
+ this.name = "SiteError";
15
+ }
16
+ }
17
+ /** Adresse désignée par l'en-tête `Location` d'une redirection, résolue par rapport à la requête. */
18
+ function redirectTarget(response, requestUrl) {
19
+ const location = response.headers.get("location");
20
+ if (location === null)
21
+ return "an address it does not give";
22
+ try {
23
+ return new URL(location, requestUrl).href;
24
+ }
25
+ catch {
26
+ return location;
27
+ }
28
+ }
29
+ export class OrganizerSiteClient {
30
+ connection;
31
+ constructor(connection) {
32
+ this.connection = connection;
33
+ }
34
+ /** Exécute la requête d'un outil et renvoie le texte rédigé pour l'agent. */
35
+ async call(request) {
36
+ return (await this.envelope(request)).text;
37
+ }
38
+ /**
39
+ * Attente longue des demandes qui passent en attente (`GET requests/wait`) ;
40
+ * le délai de l'appel couvre la durée de l'attente.
41
+ */
42
+ async waitForRequests(query) {
43
+ const params = { timeout_seconds: String(query.timeout_seconds) };
44
+ if (query.since !== undefined)
45
+ params.since = query.since;
46
+ if (query.watcher)
47
+ params.watcher = "true";
48
+ const envelope = await this.envelope({ method: "GET", path: "requests/wait", query: params, body: undefined }, query.timeout_seconds * 1000 + REQUEST_TIMEOUT_MS);
49
+ return this.parseData(WaitRequestsDataSchema, envelope.data);
50
+ }
51
+ /** Données structurées d'une réponse, validées par `schema`. */
52
+ parseData(schema, data) {
53
+ const parsed = schema.safeParse(data);
54
+ if (!parsed.success) {
55
+ throw new SiteError(`The organizer site at ${this.connection.url} sent an unexpected answer. Check that the site runs the same version of @imenam/mcp-organizer.`);
56
+ }
57
+ return parsed.data;
58
+ }
59
+ /** Exécute une requête de l'API agent et renvoie sa réponse : texte rédigé pour l'agent et données structurées. */
60
+ async envelope(request, timeoutMs = REQUEST_TIMEOUT_MS) {
61
+ const url = new URL(`${this.connection.url}/api/agent/${request.path}`);
62
+ for (const [name, value] of Object.entries(request.query))
63
+ url.searchParams.set(name, value);
64
+ let response;
65
+ try {
66
+ response = await fetch(url, {
67
+ method: request.method,
68
+ headers: {
69
+ authorization: `Bearer ${this.connection.token}`,
70
+ accept: "application/json",
71
+ ...(request.body === undefined ? {} : { "content-type": "application/json" }),
72
+ },
73
+ body: request.body === undefined ? undefined : JSON.stringify(request.body),
74
+ // Une redirection n'est jamais suivie : le jeton ne part que vers ORGANIZER_URL.
75
+ redirect: "manual",
76
+ signal: AbortSignal.timeout(timeoutMs),
77
+ });
78
+ }
79
+ catch (error) {
80
+ const reason = error?.cause?.message ?? error?.message ?? String(error);
81
+ throw new SiteError(`Cannot reach the organizer site at ${this.connection.url} (${reason}). Check ORGANIZER_URL and that the site is running, then retry.`);
82
+ }
83
+ if (response.status >= 300 && response.status < 400) {
84
+ throw new SiteError(`The organizer site at ${this.connection.url} answered with a redirect (HTTP ${response.status}) to ${redirectTarget(response, url)}: the request was not sent there. Use the final https URL of the site (its ORGANIZER_PUBLIC_URL) as ORGANIZER_URL.`, response.status);
85
+ }
86
+ const text = await response.text();
87
+ let payload = null;
88
+ try {
89
+ payload = text === "" ? null : JSON.parse(text);
90
+ }
91
+ catch {
92
+ payload = null;
93
+ }
94
+ if (!response.ok) {
95
+ const error = AgentErrorSchema.safeParse(payload);
96
+ if (error.success)
97
+ throw new SiteError(error.data.error, response.status);
98
+ throw new SiteError(`The organizer site at ${this.connection.url} answered HTTP ${response.status} without an explanation. Check ORGANIZER_URL (it must be the public URL of the site).`, response.status);
99
+ }
100
+ const envelope = AgentEnvelopeSchema.safeParse(payload);
101
+ if (!envelope.success) {
102
+ throw new SiteError(`The organizer site at ${this.connection.url} sent an unexpected answer. Check ORGANIZER_URL and that the site runs the same version of @imenam/mcp-organizer.`);
103
+ }
104
+ return envelope.data;
105
+ }
106
+ }
@@ -0,0 +1,13 @@
1
+ import type { OrganizerSiteClient, SiteConnection } from "./client.js";
2
+ import type { ToolDefinition } from "./tools.js";
3
+ /**
4
+ * Outil `enable_notifications`, exécuté par le mode MCP lui-même : il lit le
5
+ * jeton, l'espace et l'état du veilleur sur le site, dépose le relais du
6
+ * veilleur et rend l'appel `Monitor` que l'agent doit faire.
7
+ */
8
+ /** Durée maximale d'un `Monitor` côté hôte. */
9
+ export declare const MONITOR_TIMEOUT_MS = 1800000;
10
+ export declare const ENABLE_NOTIFICATIONS_TOOL: ToolDefinition;
11
+ /** Commande du veilleur du jeton `tokenId`. */
12
+ export declare function watchCommand(tokenId: string): string;
13
+ export declare function enableNotifications(client: OrganizerSiteClient, connection: SiteConnection): Promise<string>;