@imenam/mcp-organizer 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +529 -2
- package/dist/shared/logic/first-account.d.ts +13 -0
- package/dist/shared/logic/first-account.js +15 -0
- package/dist/shared/logic/labels.d.ts +5 -0
- package/dist/shared/logic/labels.js +17 -0
- package/dist/shared/logic/positions.d.ts +9 -0
- package/dist/shared/logic/positions.js +19 -0
- package/dist/shared/logic/spaces.d.ts +13 -0
- package/dist/shared/logic/spaces.js +48 -0
- package/dist/shared/logic/test-summary.d.ts +28 -0
- package/dist/shared/logic/test-summary.js +35 -0
- package/dist/shared/schemas/agent-api.schema.d.ts +268 -0
- package/dist/shared/schemas/agent-api.schema.js +94 -0
- package/dist/shared/schemas/agent-tools.schema.d.ts +724 -0
- package/dist/shared/schemas/agent-tools.schema.js +165 -0
- package/dist/shared/schemas/api.schema.d.ts +249 -0
- package/dist/shared/schemas/api.schema.js +116 -0
- package/dist/shared/schemas/entities.schema.d.ts +472 -0
- package/dist/shared/schemas/entities.schema.js +262 -0
- package/dist/shared/schemas/enums.schema.d.ts +135 -0
- package/dist/shared/schemas/enums.schema.js +58 -0
- package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
- package/dist/shared/schemas/gui-api.schema.js +20 -0
- package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
- package/dist/shared/schemas/ideas-api.schema.js +40 -0
- package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
- package/dist/shared/schemas/notifications-api.schema.js +58 -0
- package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
- package/dist/shared/schemas/tasks-api.schema.js +225 -0
- package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
- package/dist/shared/schemas/tokens-api.schema.js +42 -0
- package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
- package/dist/shared/schemas/transcription-api.schema.js +12 -0
- package/dist/src/agent-api/checklist.d.ts +3 -0
- package/dist/src/agent-api/checklist.js +127 -0
- package/dist/src/agent-api/ideas.d.ts +3 -0
- package/dist/src/agent-api/ideas.js +93 -0
- package/dist/src/agent-api/requests.d.ts +3 -0
- package/dist/src/agent-api/requests.js +60 -0
- package/dist/src/agent-api/router.d.ts +3 -0
- package/dist/src/agent-api/router.js +40 -0
- package/dist/src/agent-api/tasks.d.ts +2 -0
- package/dist/src/agent-api/tasks.js +290 -0
- package/dist/src/agent-api/types.d.ts +41 -0
- package/dist/src/agent-api/types.js +14 -0
- package/dist/src/cli/backup.d.ts +12 -0
- package/dist/src/cli/backup.js +126 -0
- package/dist/src/cli/prompt.d.ts +7 -0
- package/dist/src/cli/prompt.js +40 -0
- package/dist/src/cli/set-user.d.ts +2 -0
- package/dist/src/cli/set-user.js +114 -0
- package/dist/src/cli/setup-mcp.d.ts +10 -0
- package/dist/src/cli/setup-mcp.js +110 -0
- package/dist/src/cli/setup.d.ts +39 -0
- package/dist/src/cli/setup.js +626 -0
- package/dist/src/core/accounts.d.ts +129 -0
- package/dist/src/core/accounts.js +284 -0
- package/dist/src/core/agent-tokens.d.ts +31 -0
- package/dist/src/core/agent-tokens.js +120 -0
- package/dist/src/core/agent.d.ts +71 -0
- package/dist/src/core/agent.js +358 -0
- package/dist/src/core/attachments.d.ts +21 -0
- package/dist/src/core/attachments.js +44 -0
- package/dist/src/core/data-dir.d.ts +5 -0
- package/dist/src/core/data-dir.js +11 -0
- package/dist/src/core/db.d.ts +13 -0
- package/dist/src/core/db.js +38 -0
- package/dist/src/core/env-file.d.ts +11 -0
- package/dist/src/core/env-file.js +36 -0
- package/dist/src/core/errors.d.ts +24 -0
- package/dist/src/core/errors.js +42 -0
- package/dist/src/core/history.d.ts +34 -0
- package/dist/src/core/history.js +279 -0
- package/dist/src/core/ideas.d.ts +16 -0
- package/dist/src/core/ideas.js +100 -0
- package/dist/src/core/ids.d.ts +2 -0
- package/dist/src/core/ids.js +5 -0
- package/dist/src/core/log-dir.d.ts +2 -0
- package/dist/src/core/log-dir.js +9 -0
- package/dist/src/core/logger.d.ts +14 -0
- package/dist/src/core/logger.js +37 -0
- package/dist/src/core/mail.d.ts +43 -0
- package/dist/src/core/mail.js +44 -0
- package/dist/src/core/mcp-entry.d.ts +20 -0
- package/dist/src/core/mcp-entry.js +15 -0
- package/dist/src/core/migrations.d.ts +18 -0
- package/dist/src/core/migrations.js +404 -0
- package/dist/src/core/native-sqlite.d.ts +2 -0
- package/dist/src/core/native-sqlite.js +23 -0
- package/dist/src/core/notifications.d.ts +46 -0
- package/dist/src/core/notifications.js +173 -0
- package/dist/src/core/project-root.d.ts +2 -0
- package/dist/src/core/project-root.js +13 -0
- package/dist/src/core/requests.d.ts +49 -0
- package/dist/src/core/requests.js +239 -0
- package/dist/src/core/snapshots.d.ts +32 -0
- package/dist/src/core/snapshots.js +35 -0
- package/dist/src/core/spaces.d.ts +66 -0
- package/dist/src/core/spaces.js +252 -0
- package/dist/src/core/store.d.ts +565 -0
- package/dist/src/core/store.js +2093 -0
- package/dist/src/core/task-history.d.ts +7 -0
- package/dist/src/core/task-history.js +41 -0
- package/dist/src/core/tasks.d.ts +58 -0
- package/dist/src/core/tasks.js +389 -0
- package/dist/src/core/test-tasks.d.ts +19 -0
- package/dist/src/core/test-tasks.js +135 -0
- package/dist/src/core/transcription.d.ts +45 -0
- package/dist/src/core/transcription.js +92 -0
- package/dist/src/deploy/env-render.d.ts +26 -0
- package/dist/src/deploy/env-render.js +98 -0
- package/dist/src/deploy/templates.d.ts +38 -0
- package/dist/src/deploy/templates.js +130 -0
- package/dist/src/gui-api.d.ts +37 -0
- package/dist/src/gui-api.js +383 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +100 -0
- package/dist/src/mcp/client.d.ts +23 -0
- package/dist/src/mcp/client.js +81 -0
- package/dist/src/mcp/start.d.ts +14 -0
- package/dist/src/mcp/start.js +104 -0
- package/dist/src/mcp/tools.d.ts +46 -0
- package/dist/src/mcp/tools.js +348 -0
- package/dist/src/server/app.d.ts +25 -0
- package/dist/src/server/app.js +290 -0
- package/dist/src/server/config.d.ts +23 -0
- package/dist/src/server/config.js +75 -0
- package/dist/src/server/limits.d.ts +2 -0
- package/dist/src/server/limits.js +4 -0
- package/dist/src/server/login-limiter.d.ts +36 -0
- package/dist/src/server/login-limiter.js +81 -0
- package/dist/src/server/start.d.ts +5 -0
- package/dist/src/server/start.js +88 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +16 -0
- package/gui/dist/assets/index-DwkXx4Q2.js +70 -0
- package/gui/dist/assets/index-lnvnkDdg.css +1 -0
- package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/gui/dist/index.html +13 -0
- 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,100 @@
|
|
|
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 --version Affiche la version.
|
|
23
|
+
mcp-organizer --help Affiche cette aide.
|
|
24
|
+
|
|
25
|
+
Option commune : --dotenv=<fichier> lit ce fichier .env. Sans elle, le site et ses commandes
|
|
26
|
+
lisent <package>/.env ; le mode MCP n'en lit aucun.
|
|
27
|
+
Le site exige PORT et ORGANIZER_PUBLIC_URL ; HOST vaut 127.0.0.1 par défaut.
|
|
28
|
+
Le mode MCP exige ORGANIZER_URL (URL publique du site, en https hors de la machine locale)
|
|
29
|
+
et ORGANIZER_TOKEN (jeton d'agent), fournis par son environnement.
|
|
30
|
+
Voir le README pour la liste complète des variables.`;
|
|
31
|
+
/** Commandes qui ouvrent la base du site ; toute autre invocation démarre le mode MCP. */
|
|
32
|
+
const SITE_COMMANDS = ["--server", "--set-user", "--setup", "--backup"];
|
|
33
|
+
async function main() {
|
|
34
|
+
const argv = process.argv.slice(2);
|
|
35
|
+
if (argv.includes("--setup") && (argv.includes("--help") || argv.includes("-h"))) {
|
|
36
|
+
const { SETUP_USAGE } = await import("./cli/setup.js");
|
|
37
|
+
process.stdout.write(`${SETUP_USAGE}\n`);
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
if (argv.includes("--help") || argv.includes("-h")) {
|
|
41
|
+
process.stdout.write(`${USAGE}\n`);
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
if (argv.includes("--version") || argv.includes("-v")) {
|
|
45
|
+
process.stdout.write(`${APP_VERSION}\n`);
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
// La commande de setup s'exécute dans le projet de l'utilisateur, avant toute lecture de configuration.
|
|
49
|
+
if (argv.includes("--claude-setup-mcp")) {
|
|
50
|
+
const { runSetupMcp } = await import("./cli/setup-mcp.js");
|
|
51
|
+
process.exit(runSetupMcp(argv, process.cwd()));
|
|
52
|
+
}
|
|
53
|
+
const rootDir = findProjectRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
54
|
+
const siteCommand = SITE_COMMANDS.some((flag) => argv.includes(flag));
|
|
55
|
+
// Les commandes du site ouvrent la base : better-sqlite3 doit avoir son module natif.
|
|
56
|
+
if (siteCommand) {
|
|
57
|
+
const { assertSqliteLoadable } = await import("./core/native-sqlite.js");
|
|
58
|
+
await assertSqliteLoadable();
|
|
59
|
+
}
|
|
60
|
+
// Le setup écrit le .env : il le relit lui-même, sans le charger dans l'environnement.
|
|
61
|
+
if (argv.includes("--setup")) {
|
|
62
|
+
const { runSetup } = await import("./cli/setup.js");
|
|
63
|
+
process.exit(await runSetup(argv, rootDir));
|
|
64
|
+
}
|
|
65
|
+
// Le mode MCP tient sa configuration de l'environnement que lui donne l'agent ou la gateway ;
|
|
66
|
+
// il ne lit un fichier .env que désigné par --dotenv.
|
|
67
|
+
try {
|
|
68
|
+
const envFile = resolveEnvFile(argv, rootDir);
|
|
69
|
+
if (siteCommand || envFile.required)
|
|
70
|
+
loadEnvFile(envFile);
|
|
71
|
+
}
|
|
72
|
+
catch (err) {
|
|
73
|
+
throw new ConfigError(err?.message ?? String(err));
|
|
74
|
+
}
|
|
75
|
+
const args = withoutEnvFileFlag(argv);
|
|
76
|
+
if (args.includes("--set-user")) {
|
|
77
|
+
const [{ runSetUser }, { resolveDataDir }] = await Promise.all([import("./cli/set-user.js"), import("./core/data-dir.js")]);
|
|
78
|
+
process.exit(await runSetUser(args, resolveDataDir(rootDir)));
|
|
79
|
+
}
|
|
80
|
+
if (args.includes("--backup")) {
|
|
81
|
+
const [{ runBackup }, { resolveDataDir }] = await Promise.all([import("./cli/backup.js"), import("./core/data-dir.js")]);
|
|
82
|
+
process.exit(await runBackup(args, resolveDataDir(rootDir)));
|
|
83
|
+
}
|
|
84
|
+
if (args.includes("--server")) {
|
|
85
|
+
const { runServerMode } = await import("./server/start.js");
|
|
86
|
+
await runServerMode(rootDir);
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
const { runMcpMode } = await import("./mcp/start.js");
|
|
90
|
+
await runMcpMode();
|
|
91
|
+
}
|
|
92
|
+
process.on("uncaughtException", (err) => {
|
|
93
|
+
process.stderr.write(`Uncaught exception: ${err.stack ?? err.message}\n`);
|
|
94
|
+
process.exit(1);
|
|
95
|
+
});
|
|
96
|
+
main().catch((err) => {
|
|
97
|
+
// Erreur de configuration : le message suffit, la pile d'appels n'aide personne.
|
|
98
|
+
process.stderr.write(err instanceof ConfigError ? `${err.message}\n` : `Fatal error: ${err?.stack ?? err}\n`);
|
|
99
|
+
process.exit(1);
|
|
100
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { ToolHttpRequest } from "./tools.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. */
|
|
7
|
+
export declare const REQUEST_TIMEOUT_MS = 30000;
|
|
8
|
+
/** Échec d'un appel : refus du site (avec son message) ou site injoignable. */
|
|
9
|
+
export declare class SiteError extends Error {
|
|
10
|
+
readonly status?: number | undefined;
|
|
11
|
+
constructor(message: string, status?: number | undefined);
|
|
12
|
+
}
|
|
13
|
+
export interface SiteConnection {
|
|
14
|
+
/** URL publique du site, sans barre oblique finale. */
|
|
15
|
+
url: string;
|
|
16
|
+
token: string;
|
|
17
|
+
}
|
|
18
|
+
export declare class OrganizerSiteClient {
|
|
19
|
+
private readonly connection;
|
|
20
|
+
constructor(connection: SiteConnection);
|
|
21
|
+
/** Exécute la requête d'un outil et renvoie le texte rédigé pour l'agent. */
|
|
22
|
+
call(request: ToolHttpRequest): Promise<string>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { AgentEnvelopeSchema, AgentErrorSchema } 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. */
|
|
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
|
+
const url = new URL(`${this.connection.url}/api/agent/${request.path}`);
|
|
37
|
+
for (const [name, value] of Object.entries(request.query))
|
|
38
|
+
url.searchParams.set(name, value);
|
|
39
|
+
let response;
|
|
40
|
+
try {
|
|
41
|
+
response = await fetch(url, {
|
|
42
|
+
method: request.method,
|
|
43
|
+
headers: {
|
|
44
|
+
authorization: `Bearer ${this.connection.token}`,
|
|
45
|
+
accept: "application/json",
|
|
46
|
+
...(request.body === undefined ? {} : { "content-type": "application/json" }),
|
|
47
|
+
},
|
|
48
|
+
body: request.body === undefined ? undefined : JSON.stringify(request.body),
|
|
49
|
+
// Une redirection n'est jamais suivie : le jeton ne part que vers ORGANIZER_URL.
|
|
50
|
+
redirect: "manual",
|
|
51
|
+
signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const reason = error?.cause?.message ?? error?.message ?? String(error);
|
|
56
|
+
throw new SiteError(`Cannot reach the organizer site at ${this.connection.url} (${reason}). Check ORGANIZER_URL and that the site is running, then retry.`);
|
|
57
|
+
}
|
|
58
|
+
if (response.status >= 300 && response.status < 400) {
|
|
59
|
+
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);
|
|
60
|
+
}
|
|
61
|
+
const text = await response.text();
|
|
62
|
+
let payload = null;
|
|
63
|
+
try {
|
|
64
|
+
payload = text === "" ? null : JSON.parse(text);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
payload = null;
|
|
68
|
+
}
|
|
69
|
+
if (!response.ok) {
|
|
70
|
+
const error = AgentErrorSchema.safeParse(payload);
|
|
71
|
+
if (error.success)
|
|
72
|
+
throw new SiteError(error.data.error, response.status);
|
|
73
|
+
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);
|
|
74
|
+
}
|
|
75
|
+
const envelope = AgentEnvelopeSchema.safeParse(payload);
|
|
76
|
+
if (!envelope.success) {
|
|
77
|
+
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.`);
|
|
78
|
+
}
|
|
79
|
+
return envelope.data.text;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type SiteConnection } from "./client.js";
|
|
2
|
+
/** Présentation du serveur transmise au client MCP : concepts et déroulé. */
|
|
3
|
+
export declare const INSTRUCTIONS = "mcp-organizer connects you to an organizer site: a multi-user task manager where people organize tasks in a web app and queue requests for the AI. You, the connected agent, process those requests when the user asks you to. The site never calls an LLM: you are the intelligence.\n\nYour token covers exactly one space, with the rights of the user who created the token. Every tool works on that space.\n\nConcepts:\n- A **space** holds tasks and an idea box. A user's **personal space** is private; a **shared space** has one owner and members.\n- A **task** has a title, a markdown description, a status (todo, doing, review, done), a priority (low, medium, high, critical), assignees and tags. Tasks nest on two levels: task > sub-task > sub-sub-task. A task is either standard (with a checklist) or a test (with checkpoints that each tester marks ok, ko, blocked or untested).\n- A **request** is something a member of your space asked the AI: an idea, a remark, a test review, a message or an idea promotion. It is pending, then processing once you claim it (only your token can then act for it), then done (with your reply) or failed (with the reason).\n- A **batch** is the set of actions you perform for one request. Every action is logged with its before and after state and with your token, and users can undo one action or the whole batch.\n- The **scope** of your work is the space of your token: you see and process the requests of all its members, and you never act outside it.\n\nWork loop, when the user asks you to process the queue:\n1. list_requests to see the pending requests of your space.\n2. claim_request on the oldest one: it becomes processing for you and you get its context (text, author, context task, task tree, members, recent ideas). get_context shows the space at any time.\n3. Read what you need with get_task and search_tasks.\n4. Act with create_task, update_task, set_assignees, set_tags, edit_checklist, add_comment, create_idea, update_idea, promote_idea \u2014 always passing the request_id. Each write is one undoable action of the batch.\n5. complete_request with a short reply in the author's language (or fail_request with the reason), then go back to step 1 until no request is pending.\nNothing arrives on its own: there is no waiting tool. When the queue is empty, tell the user.\n\nHow to handle each kind of request:\n- **idea**: classify it. If it belongs to an existing task or sub-task, attach it there (sub-task, checklist item or comment). If it is actionable and important enough, create a task (or sub-task) with a fitting priority. Otherwise, put it in the idea box with create_idea. Prefer existing tasks over duplicates.\n- **test_review**: feed the tester's findings back into the existing tasks (status, checklist, comments, priority of the affected tasks), or create a task for each problem that has none.\n- **remark** and **message**: do whatever is useful \u2014 answer, update or create tasks, comment \u2014 and say what you did in the reply.\n- **idea_promotion**: turn the context idea into a well-formed task with promote_idea.\n\nRules: write titles, descriptions and replies in the language of the users; keep the reply short and factual. Errors tell you what to do next; an error about the token (missing, revoked, expired, owner no longer a member) cannot be fixed by retrying: tell the user.";
|
|
4
|
+
/**
|
|
5
|
+
* Connexion au site d'après `ORGANIZER_URL` et `ORGANIZER_TOKEN` ; une valeur
|
|
6
|
+
* absente ou invalide, ou une URL http vers une autre machine, est une `ConfigError`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function loadSiteConnection(): SiteConnection;
|
|
9
|
+
/**
|
|
10
|
+
* Mode MCP (par défaut) : serveur stdio sans base ni logique métier. Il
|
|
11
|
+
* expose les outils de l'agent et relaie chaque appel à l'API agent du site
|
|
12
|
+
* désigné par `ORGANIZER_URL`, avec le jeton `ORGANIZER_TOKEN`.
|
|
13
|
+
*/
|
|
14
|
+
export declare function runMcpMode(): Promise<void>;
|