@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.
Files changed (155) hide show
  1. package/README.md +529 -2
  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 +268 -0
  13. package/dist/shared/schemas/agent-api.schema.js +94 -0
  14. package/dist/shared/schemas/agent-tools.schema.d.ts +724 -0
  15. package/dist/shared/schemas/agent-tools.schema.js +165 -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/requests.d.ts +3 -0
  39. package/dist/src/agent-api/requests.js +60 -0
  40. package/dist/src/agent-api/router.d.ts +3 -0
  41. package/dist/src/agent-api/router.js +40 -0
  42. package/dist/src/agent-api/tasks.d.ts +2 -0
  43. package/dist/src/agent-api/tasks.js +290 -0
  44. package/dist/src/agent-api/types.d.ts +41 -0
  45. package/dist/src/agent-api/types.js +14 -0
  46. package/dist/src/cli/backup.d.ts +12 -0
  47. package/dist/src/cli/backup.js +126 -0
  48. package/dist/src/cli/prompt.d.ts +7 -0
  49. package/dist/src/cli/prompt.js +40 -0
  50. package/dist/src/cli/set-user.d.ts +2 -0
  51. package/dist/src/cli/set-user.js +114 -0
  52. package/dist/src/cli/setup-mcp.d.ts +10 -0
  53. package/dist/src/cli/setup-mcp.js +110 -0
  54. package/dist/src/cli/setup.d.ts +39 -0
  55. package/dist/src/cli/setup.js +626 -0
  56. package/dist/src/core/accounts.d.ts +129 -0
  57. package/dist/src/core/accounts.js +284 -0
  58. package/dist/src/core/agent-tokens.d.ts +31 -0
  59. package/dist/src/core/agent-tokens.js +120 -0
  60. package/dist/src/core/agent.d.ts +71 -0
  61. package/dist/src/core/agent.js +358 -0
  62. package/dist/src/core/attachments.d.ts +21 -0
  63. package/dist/src/core/attachments.js +44 -0
  64. package/dist/src/core/data-dir.d.ts +5 -0
  65. package/dist/src/core/data-dir.js +11 -0
  66. package/dist/src/core/db.d.ts +13 -0
  67. package/dist/src/core/db.js +38 -0
  68. package/dist/src/core/env-file.d.ts +11 -0
  69. package/dist/src/core/env-file.js +36 -0
  70. package/dist/src/core/errors.d.ts +24 -0
  71. package/dist/src/core/errors.js +42 -0
  72. package/dist/src/core/history.d.ts +34 -0
  73. package/dist/src/core/history.js +279 -0
  74. package/dist/src/core/ideas.d.ts +16 -0
  75. package/dist/src/core/ideas.js +100 -0
  76. package/dist/src/core/ids.d.ts +2 -0
  77. package/dist/src/core/ids.js +5 -0
  78. package/dist/src/core/log-dir.d.ts +2 -0
  79. package/dist/src/core/log-dir.js +9 -0
  80. package/dist/src/core/logger.d.ts +14 -0
  81. package/dist/src/core/logger.js +37 -0
  82. package/dist/src/core/mail.d.ts +43 -0
  83. package/dist/src/core/mail.js +44 -0
  84. package/dist/src/core/mcp-entry.d.ts +20 -0
  85. package/dist/src/core/mcp-entry.js +15 -0
  86. package/dist/src/core/migrations.d.ts +18 -0
  87. package/dist/src/core/migrations.js +404 -0
  88. package/dist/src/core/native-sqlite.d.ts +2 -0
  89. package/dist/src/core/native-sqlite.js +23 -0
  90. package/dist/src/core/notifications.d.ts +46 -0
  91. package/dist/src/core/notifications.js +173 -0
  92. package/dist/src/core/project-root.d.ts +2 -0
  93. package/dist/src/core/project-root.js +13 -0
  94. package/dist/src/core/requests.d.ts +49 -0
  95. package/dist/src/core/requests.js +239 -0
  96. package/dist/src/core/snapshots.d.ts +32 -0
  97. package/dist/src/core/snapshots.js +35 -0
  98. package/dist/src/core/spaces.d.ts +66 -0
  99. package/dist/src/core/spaces.js +252 -0
  100. package/dist/src/core/store.d.ts +565 -0
  101. package/dist/src/core/store.js +2093 -0
  102. package/dist/src/core/task-history.d.ts +7 -0
  103. package/dist/src/core/task-history.js +41 -0
  104. package/dist/src/core/tasks.d.ts +58 -0
  105. package/dist/src/core/tasks.js +389 -0
  106. package/dist/src/core/test-tasks.d.ts +19 -0
  107. package/dist/src/core/test-tasks.js +135 -0
  108. package/dist/src/core/transcription.d.ts +45 -0
  109. package/dist/src/core/transcription.js +92 -0
  110. package/dist/src/deploy/env-render.d.ts +26 -0
  111. package/dist/src/deploy/env-render.js +98 -0
  112. package/dist/src/deploy/templates.d.ts +38 -0
  113. package/dist/src/deploy/templates.js +130 -0
  114. package/dist/src/gui-api.d.ts +37 -0
  115. package/dist/src/gui-api.js +383 -0
  116. package/dist/src/index.d.ts +2 -0
  117. package/dist/src/index.js +100 -0
  118. package/dist/src/mcp/client.d.ts +23 -0
  119. package/dist/src/mcp/client.js +81 -0
  120. package/dist/src/mcp/start.d.ts +14 -0
  121. package/dist/src/mcp/start.js +104 -0
  122. package/dist/src/mcp/tools.d.ts +46 -0
  123. package/dist/src/mcp/tools.js +348 -0
  124. package/dist/src/server/app.d.ts +25 -0
  125. package/dist/src/server/app.js +290 -0
  126. package/dist/src/server/config.d.ts +23 -0
  127. package/dist/src/server/config.js +75 -0
  128. package/dist/src/server/limits.d.ts +2 -0
  129. package/dist/src/server/limits.js +4 -0
  130. package/dist/src/server/login-limiter.d.ts +36 -0
  131. package/dist/src/server/login-limiter.js +81 -0
  132. package/dist/src/server/start.d.ts +5 -0
  133. package/dist/src/server/start.js +88 -0
  134. package/dist/src/version.d.ts +1 -0
  135. package/dist/src/version.js +16 -0
  136. package/gui/dist/assets/index-DwkXx4Q2.js +70 -0
  137. package/gui/dist/assets/index-lnvnkDdg.css +1 -0
  138. package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  139. package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  140. package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  141. package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  142. package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  143. package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  144. package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  145. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  146. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  147. package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  148. package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  149. package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  150. package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  151. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  152. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  153. package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  154. package/gui/dist/index.html +13 -0
  155. package/package.json +66 -4
@@ -0,0 +1,358 @@
1
+ import { ForbiddenError, InvariantError, NotFoundError, OrganizerError } from "./errors.js";
2
+ import { PRIORITY_LABELS, STATUS_LABELS, TYPE_LABELS } from "../../shared/logic/labels.js";
3
+ import { summarizeTest } from "../../shared/logic/test-summary.js";
4
+ export function agentActor(agent, requestId) {
5
+ return { type: "agent", requestId, tokenId: agent.token.id };
6
+ }
7
+ /**
8
+ * Exécute `fn` dans une mutation de l'agent pour la demande `requestId`. La
9
+ * demande et tout ce que `fn` lit sont vérifiés et lus dans la transaction
10
+ * de l'écriture.
11
+ */
12
+ export function actFor(store, agent, requestId, fn) {
13
+ requestOfSpace(store, agent, requestId);
14
+ return store.mutate(agentActor(agent, requestId), (tx) => fn(tx, actingRequest(tx.store, agent, requestId).space));
15
+ }
16
+ /** Demande de l'espace du jeton ; une demande d'un autre espace est introuvable pour l'agent. */
17
+ export function requestOfSpace(store, agent, requestId) {
18
+ const request = store.findAgentRequest(requestId);
19
+ if (!request || request.targetSpaceId !== agent.space.id) {
20
+ throw new NotFoundError(`Request ${requestId} not found in space "${agent.space.name}". Call list_requests to get valid request ids.`);
21
+ }
22
+ return request;
23
+ }
24
+ /**
25
+ * Demande pour laquelle l'agent veut écrire : elle vise l'espace du jeton,
26
+ * elle est `processing`, prise par ce jeton, et le propriétaire du jeton est
27
+ * encore membre de l'espace. Vérifié à chaque appel.
28
+ */
29
+ export function actingRequest(store, agent, requestId) {
30
+ const request = requestOfSpace(store, agent, requestId);
31
+ if (request.status === "pending")
32
+ throw new InvariantError(`Request ${requestId} is pending: call claim_request on it first.`);
33
+ if (request.status !== "processing") {
34
+ throw new InvariantError(`Request ${requestId} is ${request.status}, not processing: it is closed. Call list_requests to pick a pending request.`);
35
+ }
36
+ if (request.claimedByTokenId !== agent.token.id) {
37
+ throw new ForbiddenError(`Request ${requestId} was claimed by another agent token: only that agent can act for it. Call list_requests to pick a pending request.`);
38
+ }
39
+ const space = store.getSpace(request.targetSpaceId);
40
+ if (!store.isMember(space.id, agent.user.id)) {
41
+ throw new ForbiddenError(`The owner of your agent token is no longer a member of "${space.name}": the agent cannot act there.`);
42
+ }
43
+ return { request, space };
44
+ }
45
+ // --- Noms et identifiants ---------------------------------------------------------
46
+ export function userLabel(user) {
47
+ return user.displayName ?? user.email;
48
+ }
49
+ export function taskKey(store, task) {
50
+ return `${store.getSpace(task.spaceId).keyPrefix}-${task.number}`;
51
+ }
52
+ const KEY_PATTERN = /^([A-Za-z][A-Za-z0-9]{1,9})-(\d+)$/;
53
+ /** Tâche de l'espace désignée par son identifiant ou sa clé (`ORG-41`). */
54
+ export function resolveTask(store, space, ref) {
55
+ const trimmed = ref.trim();
56
+ const key = KEY_PATTERN.exec(trimmed);
57
+ let task;
58
+ if (key) {
59
+ if (key[1].toUpperCase() !== space.keyPrefix) {
60
+ throw new ForbiddenError(`Task ${trimmed} is not in your space "${space.name}" (its tasks are ${space.keyPrefix}-<number>). You can only act in this space.`);
61
+ }
62
+ task = store.findTaskByNumber(space.id, Number(key[2]));
63
+ }
64
+ else {
65
+ task = store.findTask(trimmed);
66
+ if (task && task.spaceId !== space.id)
67
+ throw new ForbiddenError(`Task ${trimmed} is not in your space "${space.name}". You can only act in this space.`);
68
+ }
69
+ if (!task)
70
+ throw new NotFoundError(`Task ${trimmed} not found in "${space.name}". Call search_tasks to find it.`);
71
+ return task;
72
+ }
73
+ /** Idée de l'espace. */
74
+ export function resolveIdea(store, space, ideaId) {
75
+ const idea = store.findIdea(ideaId.trim());
76
+ if (!idea)
77
+ throw new NotFoundError(`Idea ${ideaId} not found. The ideas of your space are listed by claim_request and get_context.`);
78
+ if (idea.spaceId !== space.id)
79
+ throw new ForbiddenError(`Idea ${ideaId} is not in your space "${space.name}". You can only act in this space.`);
80
+ return idea;
81
+ }
82
+ /** Membres de l'espace désignés par leur identifiant ou leur adresse e-mail. */
83
+ export function resolveMembers(store, space, refs) {
84
+ return refs.map((ref) => {
85
+ const trimmed = ref.trim();
86
+ const user = store.findUser(trimmed) ?? store.findUserByEmail(trimmed);
87
+ if (!user || !store.isMember(space.id, user.id)) {
88
+ const members = store.listMemberships(space.id).map((m) => {
89
+ const member = store.getUser(m.userId);
90
+ return `${userLabel(member)} <${member.email}>`;
91
+ });
92
+ throw new InvariantError(`${trimmed} is not a member of "${space.name}". Members: ${members.join(", ")}.`);
93
+ }
94
+ return user;
95
+ });
96
+ }
97
+ // --- Contexte -------------------------------------------------------------------
98
+ /** Ligne compacte d'une tâche : clé, statut, priorité, type, titre, assignés. */
99
+ function taskLine(store, space, task, names) {
100
+ const assignees = store
101
+ .listAssigneeIds(task.id)
102
+ .map((id) => names.get(id) ?? id)
103
+ .sort((a, b) => a.localeCompare(b));
104
+ const flags = [task.status, task.priority, ...(task.type === "test" ? ["test"] : [])].join(", ");
105
+ return `${" ".repeat(task.depth)}- ${space.keyPrefix}-${task.number} [${flags}] ${task.title}${assignees.length > 0 ? ` — ${assignees.join(", ")}` : ""}`;
106
+ }
107
+ const STATUS_RANK = { todo: 0, doing: 1, review: 2, done: 3 };
108
+ /** Arborescence de l'espace : chaque tâche puis ses sous-tâches, par colonne et rang. */
109
+ export function spaceTree(store, space) {
110
+ const tasks = store.listTasks(space.id);
111
+ const names = memberNames(store, space);
112
+ const children = new Map();
113
+ for (const task of tasks)
114
+ children.set(task.parentId, [...(children.get(task.parentId) ?? []), task]);
115
+ for (const list of children.values())
116
+ list.sort((a, b) => STATUS_RANK[a.status] - STATUS_RANK[b.status] || a.position - b.position);
117
+ const lines = [];
118
+ const walk = (parentId) => {
119
+ for (const task of children.get(parentId) ?? []) {
120
+ lines.push(taskLine(store, space, task, names));
121
+ walk(task.id);
122
+ }
123
+ };
124
+ walk(null);
125
+ return lines;
126
+ }
127
+ function memberNames(store, space) {
128
+ return new Map(store.listMemberships(space.id).map((m) => [m.userId, userLabel(store.getUser(m.userId))]));
129
+ }
130
+ /** Membres de l'espace : nom, adresse, rôle et identifiant (pour set_assignees). */
131
+ export function memberLines(store, space) {
132
+ return store.listMemberships(space.id).map((m) => {
133
+ const user = store.getUser(m.userId);
134
+ return `- ${userLabel(user)} <${user.email}> (${m.role}, id ${user.id})`;
135
+ });
136
+ }
137
+ /** Durée écoulée depuis `iso`, en anglais. */
138
+ export function ago(iso, now = new Date()) {
139
+ const minutes = Math.floor((now.getTime() - Date.parse(iso)) / 60_000);
140
+ if (minutes < 1)
141
+ return "just now";
142
+ if (minutes < 60)
143
+ return `${minutes} min ago`;
144
+ const hours = Math.floor(minutes / 60);
145
+ if (hours < 24)
146
+ return `${hours} h ago`;
147
+ return `${Math.floor(hours / 24)} d ago`;
148
+ }
149
+ /** Nombre d'idées récentes listées dans le contexte d'une demande. */
150
+ const CONTEXT_IDEAS = 15;
151
+ /** Résumé d'une demande pour list_requests. */
152
+ export function requestLine(store, agent, request) {
153
+ const author = store.getUser(request.authorId);
154
+ const task = request.contextTaskId === null ? null : store.findTask(request.contextTaskId);
155
+ const parts = [`- ${request.id} [${request.status}] ${request.kind} by ${userLabel(author)}, ${ago(request.createdAt)}`];
156
+ if (request.status === "processing")
157
+ parts.push(request.claimedByTokenId === agent.token.id ? "claimed by you" : "claimed by another agent");
158
+ if (task)
159
+ parts.push(`context ${taskKey(store, task)}`);
160
+ const text = request.text.trim().replace(/\s+/g, " ");
161
+ return `${parts.join(" — ")}\n "${text.length > 200 ? `${text.slice(0, 199)}…` : text}"`;
162
+ }
163
+ /** Ce que l'agent doit faire d'une demande selon son type. */
164
+ const KIND_GUIDANCE = {
165
+ idea: "An idea to classify: attach it to an existing task or sub-task (checklist item, sub-task, comment), create a new task, or put it in the idea box with create_idea, depending on its priority and nature.",
166
+ remark: "A remark: do whatever is useful with it (update tasks, add a comment, create a task), then report what you did.",
167
+ message: "A message to you: answer it and perform the actions it asks for.",
168
+ test_review: "A test review written by a tester of the context task: feed it back into the existing tasks (status, checklist, comments, priorities) or create a task for each problem found.",
169
+ idea_promotion: "Promote the context idea into a well-formed task (clear title, description, type, priority, parent if relevant) with promote_idea.",
170
+ };
171
+ /** Contexte complet d'une demande, renvoyé par claim_request. */
172
+ export function requestContext(store, agent, request) {
173
+ const author = store.getUser(request.authorId);
174
+ const target = store.getSpace(request.targetSpaceId);
175
+ const lines = [];
176
+ lines.push(`Request ${request.id} is now processing (kind: ${request.kind}), claimed by your token.`);
177
+ lines.push(KIND_GUIDANCE[request.kind]);
178
+ lines.push("");
179
+ lines.push(`Author: ${userLabel(author)} <${author.email}> (id ${author.id}).`);
180
+ lines.push(`You act with the rights of ${userLabel(agent.user)} <${agent.user.email}>, the owner of your token.`);
181
+ lines.push(`Space: "${target.name}" (${target.kind}, task keys ${target.keyPrefix}-<number>). Act only in this space.`);
182
+ if (request.contextTaskId !== null) {
183
+ const task = store.findTask(request.contextTaskId);
184
+ if (task) {
185
+ const space = store.getSpace(task.spaceId);
186
+ lines.push(`Context task: ${space.keyPrefix}-${task.number} "${task.title}" [${task.status}, ${task.priority}${task.type === "test" ? ", test" : ""}] (id ${task.id})${space.id !== target.id ? ` in space "${space.name}" (out of your reach)` : ""}.`);
187
+ if (task.type === "test" && request.kind === "test_review") {
188
+ const checkpoints = store.listCheckpoints(task.id);
189
+ const results = store.listTestResults(task.id).filter((result) => result.testerId === request.authorId);
190
+ const summary = summarizeTest(checkpoints.map((c) => c.id), results, [request.authorId]);
191
+ lines.push(`Results of the tester: ${summary.ok} ok, ${summary.ko} ko, ${summary.blocked} blocked, ${summary.untested} untested out of ${summary.total}.`);
192
+ for (const checkpoint of checkpoints) {
193
+ const result = results.find((r) => r.checkpointId === checkpoint.id);
194
+ lines.push(` - [${result?.result ?? "untested"}] ${checkpoint.text}${result?.comment ? ` — ${result.comment}` : ""}`);
195
+ }
196
+ }
197
+ }
198
+ }
199
+ if (request.contextIdeaId !== null) {
200
+ const idea = store.findIdea(request.contextIdeaId);
201
+ if (idea)
202
+ lines.push(`Context idea: ${idea.id} "${idea.text}".`);
203
+ }
204
+ lines.push("", "Text:", request.text, "");
205
+ lines.push(...spaceContextLines(store, target));
206
+ lines.push("", "Next: read with get_task / search_tasks, act with the write tools (pass this request_id), then call complete_request with a short reply in the author's language.");
207
+ return lines.join("\n");
208
+ }
209
+ /** Idées actives récentes de la boîte à idées d'un espace. */
210
+ export function recentIdeas(store, space) {
211
+ return store
212
+ .listIdeas(space.id)
213
+ .filter((idea) => idea.archivedAt === null && idea.promotedTaskId === null)
214
+ .slice(0, CONTEXT_IDEAS);
215
+ }
216
+ /** Arbre des tâches, membres et idées récentes d'un espace, pour claim_request et get_context. */
217
+ export function spaceContextLines(store, space) {
218
+ const lines = [];
219
+ const tree = spaceTree(store, space);
220
+ lines.push(`Tasks of "${space.name}" (key [status, priority] title — assignees):`);
221
+ lines.push(...(tree.length > 0 ? tree : ["(no task yet)"]));
222
+ lines.push("", "Members:", ...memberLines(store, space));
223
+ const ideas = recentIdeas(store, space);
224
+ lines.push("", "Recent ideas in the idea box:");
225
+ lines.push(...(ideas.length > 0 ? ideas.map((idea) => `- ${idea.id}: "${idea.text.trim().replace(/\s+/g, " ")}" (${ago(idea.createdAt)})`) : ["(none)"]));
226
+ return lines;
227
+ }
228
+ // --- Données structurées des réponses -----------------------------------------------
229
+ export function spaceData(space) {
230
+ return { id: space.id, name: space.name, kind: space.kind, keyPrefix: space.keyPrefix };
231
+ }
232
+ export function memberData(store, space) {
233
+ return store.listMemberships(space.id).map((m) => {
234
+ const user = store.getUser(m.userId);
235
+ return { id: user.id, name: userLabel(user), email: user.email, role: m.role };
236
+ });
237
+ }
238
+ export function taskData(store, task) {
239
+ const parent = task.parentId === null ? null : store.findTask(task.parentId);
240
+ return {
241
+ id: task.id,
242
+ key: taskKey(store, task),
243
+ title: task.title,
244
+ type: task.type,
245
+ status: task.status,
246
+ priority: task.priority,
247
+ depth: task.depth,
248
+ parentKey: parent ? taskKey(store, parent) : null,
249
+ assigneeIds: store.listAssigneeIds(task.id),
250
+ tags: store
251
+ .listTagIds(task.id)
252
+ .map((id) => store.findTag(id)?.name ?? id)
253
+ .sort((a, b) => a.localeCompare(b)),
254
+ };
255
+ }
256
+ /** Tâches d'un espace dans l'ordre de l'arbre : chaque tâche puis ses sous-tâches, par colonne et rang. */
257
+ export function treeData(store, space) {
258
+ const tasks = store.listTasks(space.id);
259
+ const children = new Map();
260
+ for (const task of tasks)
261
+ children.set(task.parentId, [...(children.get(task.parentId) ?? []), task]);
262
+ for (const list of children.values())
263
+ list.sort((a, b) => STATUS_RANK[a.status] - STATUS_RANK[b.status] || a.position - b.position);
264
+ const ordered = [];
265
+ const walk = (parentId) => {
266
+ for (const task of children.get(parentId) ?? []) {
267
+ ordered.push(taskData(store, task));
268
+ walk(task.id);
269
+ }
270
+ };
271
+ walk(null);
272
+ return ordered;
273
+ }
274
+ export function ideaData(store, idea) {
275
+ const promoted = idea.promotedTaskId === null ? null : store.findTask(idea.promotedTaskId);
276
+ return { id: idea.id, text: idea.text, archived: idea.archivedAt !== null, promotedTaskKey: promoted ? taskKey(store, promoted) : null, createdAt: idea.createdAt };
277
+ }
278
+ export function requestData(store, agent, request) {
279
+ const author = store.getUser(request.authorId);
280
+ const task = request.contextTaskId === null ? null : store.findTask(request.contextTaskId);
281
+ return {
282
+ id: request.id,
283
+ kind: request.kind,
284
+ status: request.status,
285
+ text: request.text,
286
+ author: { id: author.id, name: userLabel(author), email: author.email },
287
+ contextTask: task ? { id: task.id, key: taskKey(store, task), title: task.title } : null,
288
+ contextIdeaId: request.contextIdeaId,
289
+ createdAt: request.createdAt,
290
+ claimedAt: request.claimedAt,
291
+ claimedByThisToken: request.claimedByTokenId === agent.token.id,
292
+ reply: request.reply,
293
+ failureReason: request.failureReason,
294
+ };
295
+ }
296
+ // --- Résumés des actions (affichés dans la GUI, en français) ----------------------
297
+ export function quote(text, max = 60) {
298
+ const line = text.trim().split(/\r?\n/)[0];
299
+ return `« ${line.length > max ? `${line.slice(0, max - 1)}…` : line} »`;
300
+ }
301
+ /** Résumé d'une création de tâche. */
302
+ export function createdTaskSummary(store, task) {
303
+ const what = task.type === "test" ? "Test créé" : task.depth === 0 ? "Tâche créée" : task.depth === 1 ? "Sous-tâche créée" : "Sous-sous-tâche créée";
304
+ const parent = task.parentId === null ? null : store.findTask(task.parentId);
305
+ return `${what} : ${quote(task.title)}${parent ? ` sous ${taskKey(store, parent)}` : ""}`;
306
+ }
307
+ /** Résumé des champs changés d'une tâche, et nature de l'opération. */
308
+ export function taskUpdateSummary(store, before, after) {
309
+ const parts = [];
310
+ if (before.title !== after.title)
311
+ parts.push(`Titre ${quote(after.title)}`);
312
+ if (before.description !== after.description)
313
+ parts.push("Description modifiée");
314
+ if (before.status !== after.status)
315
+ parts.push(`Statut ${STATUS_LABELS[before.status]} → ${STATUS_LABELS[after.status]}`);
316
+ if (before.priority !== after.priority)
317
+ parts.push(`Priorité ${PRIORITY_LABELS[before.priority]} → ${PRIORITY_LABELS[after.priority]}`);
318
+ if (before.type !== after.type)
319
+ parts.push(`Type ${TYPE_LABELS[before.type]} → ${TYPE_LABELS[after.type]}`);
320
+ if (before.parentId !== after.parentId) {
321
+ const parent = after.parentId === null ? null : store.findTask(after.parentId);
322
+ parts.push(parent ? `Déplacée sous ${taskKey(store, parent)}` : "Remontée au premier niveau");
323
+ }
324
+ if (before.testedTaskId !== after.testedTaskId) {
325
+ const tested = after.testedTaskId === null ? null : store.findTask(after.testedTaskId);
326
+ parts.push(tested ? `Teste ${taskKey(store, tested)}` : "Tâche testée retirée");
327
+ }
328
+ const onlyPlacement = before.title === after.title &&
329
+ before.description === after.description &&
330
+ before.priority === after.priority &&
331
+ before.type === after.type &&
332
+ before.testedTaskId === after.testedTaskId;
333
+ if (parts.length === 0 && before.position !== after.position)
334
+ parts.push("Position changée");
335
+ return { summary: parts.join(" · ") || "Aucun changement", operation: onlyPlacement ? "move" : "update" };
336
+ }
337
+ /** Résumé d'un changement d'assignés. */
338
+ export function assigneesSummary(store, before, after, test) {
339
+ const name = (id) => {
340
+ const user = store.findUser(id);
341
+ return user ? userLabel(user) : id;
342
+ };
343
+ const added = after.filter((id) => !before.includes(id)).map(name);
344
+ const removed = before.filter((id) => !after.includes(id)).map(name);
345
+ const parts = [];
346
+ if (added.length > 0)
347
+ parts.push(`${test ? "Testeur ajouté" : "Assigné à"} ${added.join(", ")}`);
348
+ if (removed.length > 0)
349
+ parts.push(`${test ? "Testeur retiré" : "Désassigné"} : ${removed.join(", ")}`);
350
+ return parts.join(" · ") || "Assignés inchangés";
351
+ }
352
+ /** Vérifie qu'un texte libre n'est pas vide ; le message dit quoi fournir. */
353
+ export function requireText(value, what) {
354
+ const text = value.trim();
355
+ if (text === "")
356
+ throw new OrganizerError(`The ${what} is empty: provide a non-empty ${what}.`);
357
+ return text;
358
+ }
@@ -0,0 +1,21 @@
1
+ import type { Attachment } from "../../shared/schemas/entities.schema.js";
2
+ /** Sous-répertoire des pièces jointes dans le répertoire de données. */
3
+ export declare const ATTACHMENTS_DIRNAME = "attachments";
4
+ /**
5
+ * Fichiers des pièces jointes, rangés sous `<dataDir>/attachments/<id>`. La
6
+ * base enregistre le chemin relatif au répertoire de données.
7
+ */
8
+ export declare class AttachmentStorage {
9
+ readonly dataDir: string;
10
+ constructor(dataDir: string);
11
+ /** Chemin relatif, enregistré en base, du fichier d'une pièce jointe. */
12
+ storagePathOf(id: string): string;
13
+ private absolute;
14
+ /** Écrit le contenu d'une pièce jointe et renvoie son chemin relatif. */
15
+ write(id: string, content: Buffer): string;
16
+ read(attachment: Attachment): Buffer;
17
+ /** Vrai si le fichier d'une pièce jointe existe. */
18
+ exists(storagePath: string): boolean;
19
+ /** Efface les fichiers de pièces jointes supprimées de la base ; un fichier déjà absent est ignoré. */
20
+ remove(storagePaths: readonly string[]): void;
21
+ }
@@ -0,0 +1,44 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /** Sous-répertoire des pièces jointes dans le répertoire de données. */
4
+ export const ATTACHMENTS_DIRNAME = "attachments";
5
+ /**
6
+ * Fichiers des pièces jointes, rangés sous `<dataDir>/attachments/<id>`. La
7
+ * base enregistre le chemin relatif au répertoire de données.
8
+ */
9
+ export class AttachmentStorage {
10
+ dataDir;
11
+ constructor(dataDir) {
12
+ this.dataDir = dataDir;
13
+ }
14
+ /** Chemin relatif, enregistré en base, du fichier d'une pièce jointe. */
15
+ storagePathOf(id) {
16
+ return `${ATTACHMENTS_DIRNAME}/${id}`;
17
+ }
18
+ absolute(storagePath) {
19
+ const resolved = path.resolve(this.dataDir, storagePath);
20
+ const root = path.resolve(this.dataDir, ATTACHMENTS_DIRNAME) + path.sep;
21
+ if (!resolved.startsWith(root))
22
+ throw new Error(`Attachment path ${storagePath} is outside of ${root}.`);
23
+ return resolved;
24
+ }
25
+ /** Écrit le contenu d'une pièce jointe et renvoie son chemin relatif. */
26
+ write(id, content) {
27
+ const storagePath = this.storagePathOf(id);
28
+ fs.mkdirSync(path.join(this.dataDir, ATTACHMENTS_DIRNAME), { recursive: true });
29
+ fs.writeFileSync(this.absolute(storagePath), content, { flag: "wx" });
30
+ return storagePath;
31
+ }
32
+ read(attachment) {
33
+ return fs.readFileSync(this.absolute(attachment.storagePath));
34
+ }
35
+ /** Vrai si le fichier d'une pièce jointe existe. */
36
+ exists(storagePath) {
37
+ return fs.existsSync(this.absolute(storagePath));
38
+ }
39
+ /** Efface les fichiers de pièces jointes supprimées de la base ; un fichier déjà absent est ignoré. */
40
+ remove(storagePaths) {
41
+ for (const storagePath of storagePaths)
42
+ fs.rmSync(this.absolute(storagePath), { force: true });
43
+ }
44
+ }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Répertoire des données : MCP_ORGANIZER_DATA_DIR, puis MCP_DATA_DIR,
3
+ * puis <racine du package>/.organizer-data.
4
+ */
5
+ export declare function resolveDataDir(rootDir: string): string;
@@ -0,0 +1,11 @@
1
+ import path from "node:path";
2
+ /**
3
+ * Répertoire des données : MCP_ORGANIZER_DATA_DIR, puis MCP_DATA_DIR,
4
+ * puis <racine du package>/.organizer-data.
5
+ */
6
+ export function resolveDataDir(rootDir) {
7
+ const configured = process.env.MCP_ORGANIZER_DATA_DIR?.trim() || process.env.MCP_DATA_DIR?.trim();
8
+ if (configured)
9
+ return path.resolve(configured);
10
+ return path.join(rootDir, ".organizer-data");
11
+ }
@@ -0,0 +1,13 @@
1
+ import Database from "better-sqlite3";
2
+ /** Nom du fichier de la base dans le répertoire de données. */
3
+ export declare const DATABASE_FILENAME = "organizer.db";
4
+ /** Attente maximale d'un verrou tenu par un autre processus, en millisecondes. */
5
+ export declare const BUSY_TIMEOUT_MS = 5000;
6
+ /** Chemin de la base dans le répertoire de données. */
7
+ export declare function databasePath(dataDir: string): string;
8
+ /**
9
+ * Ouvre la base `<dataDir>/organizer.db` : mode WAL (plusieurs processus
10
+ * lisent et écrivent la même base), clés étrangères actives, attente des
11
+ * verrous, puis migrations.
12
+ */
13
+ export declare function openDatabase(dataDir: string): Database.Database;
@@ -0,0 +1,38 @@
1
+ import Database from "better-sqlite3";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { migrate } from "./migrations.js";
5
+ /** Nom du fichier de la base dans le répertoire de données. */
6
+ export const DATABASE_FILENAME = "organizer.db";
7
+ /** Attente maximale d'un verrou tenu par un autre processus, en millisecondes. */
8
+ export const BUSY_TIMEOUT_MS = 5000;
9
+ /** Chemin de la base dans le répertoire de données. */
10
+ export function databasePath(dataDir) {
11
+ return path.join(dataDir, DATABASE_FILENAME);
12
+ }
13
+ /**
14
+ * Ouvre la base `<dataDir>/organizer.db` : mode WAL (plusieurs processus
15
+ * lisent et écrivent la même base), clés étrangères actives, attente des
16
+ * verrous, puis migrations.
17
+ */
18
+ export function openDatabase(dataDir) {
19
+ fs.mkdirSync(dataDir, { recursive: true });
20
+ const db = new Database(databasePath(dataDir));
21
+ try {
22
+ db.pragma(`busy_timeout = ${BUSY_TIMEOUT_MS}`);
23
+ const mode = db.pragma("journal_mode = WAL", { simple: true });
24
+ if (mode !== "wal") {
25
+ throw new Error(`SQLite refused WAL mode for ${databasePath(dataDir)} (mode: ${String(mode)}). Put the data directory on a local disk.`);
26
+ }
27
+ db.pragma("foreign_keys = ON");
28
+ if (db.pragma("foreign_keys", { simple: true }) !== 1) {
29
+ throw new Error("SQLite refused to enable foreign keys.");
30
+ }
31
+ migrate(db);
32
+ return db;
33
+ }
34
+ catch (err) {
35
+ db.close();
36
+ throw err;
37
+ }
38
+ }
@@ -0,0 +1,11 @@
1
+ export interface EnvFile {
2
+ path: string;
3
+ /** Fichier imposé par `--dotenv` : son absence est une erreur. */
4
+ required: boolean;
5
+ }
6
+ /** Fichier `.env` à relire, d'après la ligne de commande. */
7
+ export declare function resolveEnvFile(argv: readonly string[], rootDir: string): EnvFile;
8
+ /** Arguments de la ligne de commande sans l'option `--dotenv`. */
9
+ export declare function withoutEnvFileFlag(argv: readonly string[]): string[];
10
+ /** Relit le fichier dans `process.env`, en écrasant les valeurs héritées. */
11
+ export declare function loadEnvFile(file: EnvFile): void;
@@ -0,0 +1,36 @@
1
+ import dotenv from "dotenv";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ /**
5
+ * Fichier `.env` relu par le serveur. Ses valeurs priment sur les variables
6
+ * héritées de la déclaration du MCP.
7
+ *
8
+ * `--dotenv=<chemin>` fixe le fichier ; il doit alors exister. Sans cette
9
+ * option, le serveur lit `<racine du package>/.env` s'il existe.
10
+ */
11
+ const ENV_FILE_FLAG = "--dotenv=";
12
+ /** Fichier `.env` à relire, d'après la ligne de commande. */
13
+ export function resolveEnvFile(argv, rootDir) {
14
+ const flags = argv.filter((arg) => arg.startsWith(ENV_FILE_FLAG));
15
+ if (flags.length > 1)
16
+ throw new Error("--dotenv is given more than once: pass a single file.");
17
+ if (flags.length === 0)
18
+ return { path: path.join(rootDir, ".env"), required: false };
19
+ const value = flags[0].slice(ENV_FILE_FLAG.length).trim();
20
+ if (value === "")
21
+ throw new Error("--dotenv expects a path: --dotenv=<path to a .env file>.");
22
+ return { path: path.resolve(value), required: true };
23
+ }
24
+ /** Arguments de la ligne de commande sans l'option `--dotenv`. */
25
+ export function withoutEnvFileFlag(argv) {
26
+ return argv.filter((arg) => !arg.startsWith(ENV_FILE_FLAG));
27
+ }
28
+ /** Relit le fichier dans `process.env`, en écrasant les valeurs héritées. */
29
+ export function loadEnvFile(file) {
30
+ if (!fs.existsSync(file.path)) {
31
+ if (file.required)
32
+ throw new Error(`The file given by --dotenv does not exist: ${file.path}. Create it or fix the path.`);
33
+ return;
34
+ }
35
+ dotenv.config({ path: file.path, override: true, quiet: true });
36
+ }
@@ -0,0 +1,24 @@
1
+ /** Erreur métier : son message dit à l'appelant ce qui ne va pas et quoi faire. */
2
+ export declare class OrganizerError extends Error {
3
+ constructor(message: string);
4
+ }
5
+ /** Entité introuvable. */
6
+ export declare class NotFoundError extends OrganizerError {
7
+ constructor(message: string);
8
+ }
9
+ /** Mutation refusée parce qu'elle violerait un invariant du modèle. */
10
+ export declare class InvariantError extends OrganizerError {
11
+ constructor(message: string);
12
+ }
13
+ /** Requête sans session valide, ou identifiants refusés. */
14
+ export declare class UnauthorizedError extends OrganizerError {
15
+ constructor(message: string);
16
+ }
17
+ /** Action refusée à l'utilisateur connecté par les règles de droits. */
18
+ export declare class ForbiddenError extends OrganizerError {
19
+ constructor(message: string);
20
+ }
21
+ /** Erreur de configuration : le message suffit, sans pile d'appels. */
22
+ export declare class ConfigError extends Error {
23
+ constructor(message: string);
24
+ }
@@ -0,0 +1,42 @@
1
+ /** Erreur métier : son message dit à l'appelant ce qui ne va pas et quoi faire. */
2
+ export class OrganizerError extends Error {
3
+ constructor(message) {
4
+ super(message);
5
+ this.name = "OrganizerError";
6
+ }
7
+ }
8
+ /** Entité introuvable. */
9
+ export class NotFoundError extends OrganizerError {
10
+ constructor(message) {
11
+ super(message);
12
+ this.name = "NotFoundError";
13
+ }
14
+ }
15
+ /** Mutation refusée parce qu'elle violerait un invariant du modèle. */
16
+ export class InvariantError extends OrganizerError {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = "InvariantError";
20
+ }
21
+ }
22
+ /** Requête sans session valide, ou identifiants refusés. */
23
+ export class UnauthorizedError extends OrganizerError {
24
+ constructor(message) {
25
+ super(message);
26
+ this.name = "UnauthorizedError";
27
+ }
28
+ }
29
+ /** Action refusée à l'utilisateur connecté par les règles de droits. */
30
+ export class ForbiddenError extends OrganizerError {
31
+ constructor(message) {
32
+ super(message);
33
+ this.name = "ForbiddenError";
34
+ }
35
+ }
36
+ /** Erreur de configuration : le message suffit, sans pile d'appels. */
37
+ export class ConfigError extends Error {
38
+ constructor(message) {
39
+ super(message);
40
+ this.name = "ConfigError";
41
+ }
42
+ }
@@ -0,0 +1,34 @@
1
+ import type { Mutation } from "./store.js";
2
+ import { type AgentAction, type TaskSnapshot } from "../../shared/schemas/entities.schema.js";
3
+ /**
4
+ * Retour à un état antérieur : annulation d'une action de l'agent, après
5
+ * comparaison de l'état courant de l'entité avec celui que l'action a
6
+ * laissé, et restauration des champs d'une version de tâche.
7
+ */
8
+ /** Champs d'une tâche comparés et restaurés par l'historique. */
9
+ declare const TASK_FIELDS: readonly ["title", "description", "status", "priority", "type", "parentId", "testedTaskId"];
10
+ /**
11
+ * Annule une action de l'agent dans la mutation `tx` (acteur utilisateur).
12
+ * L'état courant de l'entité doit être celui que l'action a laissé ; sinon le
13
+ * refus nomme l'auteur de la modification. Une création est supprimée, une
14
+ * suppression recréée, une modification ramenée à l'état d'avant.
15
+ */
16
+ export declare function undoAgentAction(tx: Mutation, action: AgentAction): void;
17
+ /** Libellés français des champs restaurables d'une tâche ; « position » est le rang dans une même colonne. */
18
+ export declare const TASK_FIELD_LABELS: Record<(typeof TASK_FIELDS)[number] | "position" | "assigneeIds" | "tagIds", string>;
19
+ /**
20
+ * Champs qui diffèrent entre deux instantanés de tâche, dans l'ordre des
21
+ * libellés. La position ne compte que dans une même colonne : un changement
22
+ * de colonne la change toujours.
23
+ */
24
+ export declare function changedTaskFields(from: TaskSnapshot, to: TaskSnapshot): (keyof typeof TASK_FIELD_LABELS)[];
25
+ /**
26
+ * Ramène une tâche aux champs de `target` (titre, description, statut,
27
+ * priorité, type, parent, tâche testée, position, assignés, tags) et renvoie
28
+ * les champs changés. La tâche reprend sa position d'alors si elle est
29
+ * libre ; revenue dans une autre colonne où cette position est prise, elle va
30
+ * en fin de colonne ; dans sa colonne, une position prise rend la
31
+ * restauration impossible.
32
+ */
33
+ export declare function restoreTaskFields(tx: Mutation, taskId: string, target: TaskSnapshot): (keyof typeof TASK_FIELD_LABELS)[];
34
+ export {};