@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,60 @@
1
+ import { ClaimRequestBodySchema, ClaimRequestDataSchema, CompleteRequestBodySchema, FailRequestBodySchema, FinishRequestDataSchema, ListRequestsDataSchema, ListRequestsQuerySchema, } from "../../shared/schemas/agent-tools.schema.js";
2
+ import { agentActor, ideaData, memberData, recentIdeas, requestContext, requestData, requestLine, requestOfSpace, spaceData, treeData } from "../core/agent.js";
3
+ import { parseInput, respond } from "./types.js";
4
+ /** Demandes de l'espace du jeton : liste, prise, fin (réussie ou en échec). */
5
+ export const requestRoutes = [
6
+ {
7
+ method: "GET",
8
+ path: "/requests",
9
+ handle: ({ store }, { agent, query }) => {
10
+ const input = parseInput(ListRequestsQuerySchema, query);
11
+ const requests = store.listAgentRequestsTargeting(agent.space.id, input.status === "all" ? null : input.status, input.limit);
12
+ const data = { space: spaceData(agent.space), requests: requests.map((request) => requestData(store, agent, request)) };
13
+ if (requests.length === 0) {
14
+ const none = input.status === "pending" ? `No pending request in "${agent.space.name}".` : `No ${input.status === "all" ? "" : `${input.status} `}request in "${agent.space.name}".`;
15
+ return respond(ListRequestsDataSchema, none, data);
16
+ }
17
+ const next = input.status === "pending" ? "\n\nNext: call claim_request with the id of the oldest request." : "";
18
+ const lines = requests.map((request) => requestLine(store, agent, request)).join("\n");
19
+ return respond(ListRequestsDataSchema, `${requests.length} request(s) in "${agent.space.name}":\n${lines}${next}`, data);
20
+ },
21
+ },
22
+ {
23
+ method: "POST",
24
+ path: "/requests/:requestId/claim",
25
+ handle: ({ store }, { agent, params, body }) => {
26
+ parseInput(ClaimRequestBodySchema, body);
27
+ requestOfSpace(store, agent, params.requestId);
28
+ const request = store.mutate(agentActor(agent, params.requestId), (tx) => tx.claimAgentRequest());
29
+ return respond(ClaimRequestDataSchema, requestContext(store, agent, request), {
30
+ request: requestData(store, agent, request),
31
+ space: spaceData(agent.space),
32
+ members: memberData(store, agent.space),
33
+ tasks: treeData(store, agent.space),
34
+ ideas: recentIdeas(store, agent.space).map((idea) => ideaData(store, idea)),
35
+ });
36
+ },
37
+ },
38
+ {
39
+ method: "POST",
40
+ path: "/requests/:requestId/complete",
41
+ handle: ({ store }, { agent, params, body }) => {
42
+ const input = parseInput(CompleteRequestBodySchema, body);
43
+ requestOfSpace(store, agent, params.requestId);
44
+ const done = store.mutate(agentActor(agent, params.requestId), (tx) => tx.completeAgentRequest(input.reply));
45
+ const actionCount = store.listAgentActions(done.id).length;
46
+ return respond(FinishRequestDataSchema, `Request ${done.id} is done (${actionCount} action(s) in its batch). Call list_requests for the next one.`, { request: requestData(store, agent, done), actionCount });
47
+ },
48
+ },
49
+ {
50
+ method: "POST",
51
+ path: "/requests/:requestId/fail",
52
+ handle: ({ store }, { agent, params, body }) => {
53
+ const input = parseInput(FailRequestBodySchema, body);
54
+ requestOfSpace(store, agent, params.requestId);
55
+ const failed = store.mutate(agentActor(agent, params.requestId), (tx) => tx.failAgentRequest(input.reason));
56
+ const actionCount = store.listAgentActions(failed.id).length;
57
+ return respond(FinishRequestDataSchema, `Request ${failed.id} is marked as failed; its author sees your reason. Call list_requests for the next one.`, { request: requestData(store, agent, failed), actionCount });
58
+ },
59
+ },
60
+ ];
@@ -0,0 +1,3 @@
1
+ import type { AgentApiContext, AgentApiRequest, AgentApiResponse } from "./types.js";
2
+ /** Traite une requête de l'API agent ; une erreur est levée telle quelle, avec la nature qui fixe son code HTTP. */
3
+ export declare function handleAgentRequest(ctx: AgentApiContext, request: AgentApiRequest): AgentApiResponse;
@@ -0,0 +1,40 @@
1
+ import { resolveAgentToken } from "../core/agent-tokens.js";
2
+ import { NotFoundError, OrganizerError } from "../core/errors.js";
3
+ import { checklistRoutes } from "./checklist.js";
4
+ import { ideaRoutes } from "./ideas.js";
5
+ import { requestRoutes } from "./requests.js";
6
+ import { taskRoutes } from "./tasks.js";
7
+ function compile(route) {
8
+ const keys = [];
9
+ const source = route.path.replace(/:([A-Za-z]+)/g, (_, key) => {
10
+ keys.push(key);
11
+ return "([^/]+)";
12
+ });
13
+ return { route, pattern: new RegExp(`^${source}$`), keys };
14
+ }
15
+ const ROUTES = [...requestRoutes, ...taskRoutes, ...checklistRoutes, ...ideaRoutes].map(compile);
16
+ /** Segment de chemin décodé ; un encodage `%` invalide est une erreur 400. */
17
+ function decodeSegment(raw) {
18
+ try {
19
+ return decodeURIComponent(raw);
20
+ }
21
+ catch {
22
+ throw new OrganizerError(`Invalid URL: a path segment is badly encoded (${raw}).`);
23
+ }
24
+ }
25
+ /** Traite une requête de l'API agent ; une erreur est levée telle quelle, avec la nature qui fixe son code HTTP. */
26
+ export function handleAgentRequest(ctx, request) {
27
+ const now = ctx.now?.() ?? new Date();
28
+ const agent = resolveAgentToken(ctx.store, request.bearer, now);
29
+ for (const candidate of ROUTES) {
30
+ if (candidate.route.method !== request.method)
31
+ continue;
32
+ const match = candidate.pattern.exec(request.path);
33
+ if (!match)
34
+ continue;
35
+ const params = {};
36
+ candidate.keys.forEach((key, index) => (params[key] = decodeSegment(match[index + 1])));
37
+ return candidate.route.handle(ctx, { agent, params, query: request.query, body: request.body });
38
+ }
39
+ throw new NotFoundError(`Unknown agent API route: ${request.method} /api/agent${request.path}. Update @imenam/mcp-organizer so that the MCP mode and the site agree.`);
40
+ }
@@ -0,0 +1,2 @@
1
+ import { type AgentRoute } from "./types.js";
2
+ export declare const taskRoutes: AgentRoute[];
@@ -0,0 +1,290 @@
1
+ import { AddCommentBodySchema, CommentDataSchema, ContextDataSchema, CreateTaskBodySchema, NoQuerySchema, SearchTasksDataSchema, SearchTasksQuerySchema, SetAssigneesBodySchema, SetTagsBodySchema, TaskDataSchema, TaskDetailDataSchema, UpdateTaskBodySchema, } from "../../shared/schemas/agent-tools.schema.js";
2
+ import { combinedResult } from "../../shared/logic/test-summary.js";
3
+ import { actFor, ago, assigneesSummary, createdTaskSummary, ideaData, memberData, recentIdeas, requireText, resolveMembers, resolveTask, spaceContextLines, spaceData, taskData, taskKey, taskUpdateSummary, treeData, userLabel, } from "../core/agent.js";
4
+ import { OrganizerError } from "../core/errors.js";
5
+ import { parseInput, respond } from "./types.js";
6
+ /** Espace du jeton et ses tâches : contexte, lecture, recherche, création, modification, assignés, tags, commentaires. */
7
+ /** Nombre de commentaires récents affichés par get_task. */
8
+ const RECENT_COMMENTS = 15;
9
+ export const taskRoutes = [
10
+ {
11
+ method: "GET",
12
+ path: "/context",
13
+ handle: ({ store }, { agent, query }) => {
14
+ parseInput(NoQuerySchema, query);
15
+ const space = agent.space;
16
+ const header = `Your space: "${space.name}" (${space.kind}, task keys ${space.keyPrefix}-<number>). You act with the rights of ${userLabel(agent.user)} <${agent.user.email}>.`;
17
+ return respond(ContextDataSchema, [header, "", ...spaceContextLines(store, space)].join("\n"), {
18
+ space: spaceData(space),
19
+ members: memberData(store, space),
20
+ tasks: treeData(store, space),
21
+ ideas: recentIdeas(store, space).map((idea) => ideaData(store, idea)),
22
+ });
23
+ },
24
+ },
25
+ {
26
+ method: "GET",
27
+ path: "/tasks/:ref",
28
+ handle: ({ store }, { agent, params, query }) => {
29
+ parseInput(NoQuerySchema, query);
30
+ const task = resolveTask(store, agent.space, params.ref);
31
+ const name = (id) => {
32
+ const user = store.findUser(id);
33
+ return user ? userLabel(user) : id;
34
+ };
35
+ const lines = [];
36
+ lines.push(`${taskKey(store, task)} "${task.title}" (id ${task.id})`);
37
+ lines.push(`Type: ${task.type} · Status: ${task.status} · Priority: ${task.priority} · Depth: ${task.depth}`);
38
+ const ancestors = [];
39
+ for (let parentId = task.parentId; parentId !== null;) {
40
+ const parent = store.getTask(parentId);
41
+ ancestors.unshift(`${taskKey(store, parent)} "${parent.title}"`);
42
+ parentId = parent.parentId;
43
+ }
44
+ if (ancestors.length > 0)
45
+ lines.push(`Parents: ${ancestors.join(" > ")}`);
46
+ const tested = task.testedTaskId === null ? null : store.findTask(task.testedTaskId);
47
+ if (tested)
48
+ lines.push(`Tests: ${taskKey(store, tested)} "${tested.title}"`);
49
+ const tests = store.listTestsOf(task.id);
50
+ if (tests.length > 0)
51
+ lines.push(`Tested by: ${tests.map((test) => `${taskKey(store, test)} "${test.title}"`).join(", ")}`);
52
+ const assignees = store.listAssigneeIds(task.id);
53
+ lines.push(`${task.type === "test" ? "Testers" : "Assignees"}: ${assignees.length > 0 ? assignees.map(name).join(", ") : "none"}`);
54
+ const summary = taskData(store, task);
55
+ lines.push(`Tags: ${summary.tags.length > 0 ? summary.tags.join(", ") : "none"}`);
56
+ lines.push(`Created by ${name(task.createdBy)} ${ago(task.createdAt)}, updated ${ago(task.updatedAt)}.`);
57
+ lines.push("", "Description:", task.description.trim() === "" ? "(empty)" : task.description);
58
+ const checklist = task.type === "standard" ? store.listChecklist(task.id) : [];
59
+ const checkpoints = [];
60
+ if (task.type === "standard") {
61
+ lines.push("", `Checklist (${checklist.filter((item) => item.done).length}/${checklist.length} done):`);
62
+ lines.push(...(checklist.length > 0 ? checklist.map((item) => `- [${item.done ? "x" : " "}] ${item.text} (id ${item.id})`) : ["(empty)"]));
63
+ }
64
+ else {
65
+ const results = store.listTestResults(task.id).filter((result) => assignees.includes(result.testerId));
66
+ const points = store.listCheckpoints(task.id);
67
+ lines.push("", `Test checkpoints (${points.length}):`);
68
+ for (const checkpoint of points) {
69
+ const own = results.filter((result) => result.checkpointId === checkpoint.id);
70
+ const combined = combinedResult(own.map((result) => result.result));
71
+ checkpoints.push({ id: checkpoint.id, text: checkpoint.text, result: combined });
72
+ const comments = own.filter((result) => result.comment.trim() !== "").map((result) => `${name(result.testerId)}: ${result.comment}`);
73
+ lines.push(`- [${combined}] ${checkpoint.text} (id ${checkpoint.id})${comments.length > 0 ? ` — ${comments.join(" | ")}` : ""}`);
74
+ }
75
+ if (points.length === 0)
76
+ lines.push("(none)");
77
+ const reviews = store.listTestReviews(task.id).filter((review) => review.submittedAt !== null);
78
+ if (reviews.length > 0) {
79
+ lines.push("", "Submitted reviews:");
80
+ for (const review of reviews)
81
+ lines.push(`- ${name(review.testerId)}, ${ago(review.submittedAt)}: ${review.text}`);
82
+ }
83
+ }
84
+ const children = store.listChildren(task.id);
85
+ if (children.length > 0) {
86
+ lines.push("", "Sub-tasks:");
87
+ for (const child of children)
88
+ lines.push(`- ${taskKey(store, child)} [${child.status}, ${child.priority}${child.type === "test" ? ", test" : ""}] ${child.title}`);
89
+ }
90
+ const comments = store.listComments(task.id).slice(-RECENT_COMMENTS);
91
+ if (comments.length > 0) {
92
+ lines.push("", "Recent comments:");
93
+ for (const comment of comments)
94
+ lines.push(`- ${comment.requestId ? "agent" : name(comment.authorId)}, ${ago(comment.createdAt)}: ${comment.body}`);
95
+ }
96
+ const attachments = store.listAttachments(task.id);
97
+ if (attachments.length > 0)
98
+ lines.push("", `Attachments: ${attachments.map((attachment) => attachment.filename).join(", ")}`);
99
+ return respond(TaskDetailDataSchema, lines.join("\n"), {
100
+ task: summary,
101
+ description: task.description,
102
+ testedTaskKey: tested ? taskKey(store, tested) : null,
103
+ checklist: checklist.map((item) => ({ id: item.id, text: item.text, done: item.done })),
104
+ checkpoints,
105
+ children: children.map((child) => taskData(store, child)),
106
+ comments: comments.map((comment) => ({
107
+ id: comment.id,
108
+ authorName: comment.requestId ? "agent" : name(comment.authorId),
109
+ byAgent: comment.requestId !== null,
110
+ body: comment.body,
111
+ createdAt: comment.createdAt,
112
+ })),
113
+ attachments: attachments.map((attachment) => attachment.filename),
114
+ });
115
+ },
116
+ },
117
+ {
118
+ method: "GET",
119
+ path: "/tasks",
120
+ handle: ({ store }, { agent, query }) => {
121
+ const input = parseInput(SearchTasksQuerySchema, query);
122
+ const space = agent.space;
123
+ const assignee = input.assignee === undefined ? null : resolveMembers(store, space, [input.assignee])[0];
124
+ const words = (input.query ?? "").toLowerCase().split(/\s+/).filter((word) => word !== "");
125
+ const matches = store.listTasks(space.id).filter((task) => {
126
+ if (input.status && task.status !== input.status)
127
+ return false;
128
+ if (input.priority && task.priority !== input.priority)
129
+ return false;
130
+ if (input.type && task.type !== input.type)
131
+ return false;
132
+ if (assignee && !store.listAssigneeIds(task.id).includes(assignee.id))
133
+ return false;
134
+ const haystack = `${taskKey(store, task)} ${task.title} ${task.description}`.toLowerCase();
135
+ return words.every((word) => haystack.includes(word));
136
+ });
137
+ if (matches.length === 0)
138
+ return respond(SearchTasksDataSchema, `No task of "${space.name}" matches. Create one with create_task if needed.`, { tasks: [], total: 0 });
139
+ const shown = matches.slice(0, input.limit);
140
+ const lines = shown.map((task) => {
141
+ const parent = task.parentId === null ? null : store.findTask(task.parentId);
142
+ return `- ${taskKey(store, task)} [${task.status}, ${task.priority}${task.type === "test" ? ", test" : ""}] ${task.title}${parent ? ` (under ${taskKey(store, parent)})` : ""}`;
143
+ });
144
+ const more = matches.length > shown.length ? `\n(${matches.length - shown.length} more: narrow the query or raise limit)` : "";
145
+ return respond(SearchTasksDataSchema, `${matches.length} task(s) in "${space.name}":\n${lines.join("\n")}${more}`, {
146
+ tasks: shown.map((task) => taskData(store, task)),
147
+ total: matches.length,
148
+ });
149
+ },
150
+ },
151
+ {
152
+ method: "POST",
153
+ path: "/tasks",
154
+ handle: ({ store }, { agent, body }) => {
155
+ const input = parseInput(CreateTaskBodySchema, body);
156
+ const title = requireText(input.title, "title");
157
+ const { parent, task } = actFor(store, agent, input.request_id, (tx, space) => {
158
+ const parent = input.parent === undefined ? null : resolveTask(tx.store, space, input.parent);
159
+ const created = tx.createTask({
160
+ spaceId: space.id,
161
+ parentId: parent?.id ?? null,
162
+ title,
163
+ description: input.description,
164
+ type: input.type,
165
+ priority: input.priority,
166
+ status: input.status,
167
+ });
168
+ tx.recordAgentAction({
169
+ entityType: "task",
170
+ entityId: created.id,
171
+ operation: "create",
172
+ before: null,
173
+ after: tx.store.taskSnapshot(created.id),
174
+ summary: createdTaskSummary(tx.store, created),
175
+ taskId: created.id,
176
+ });
177
+ return { parent, task: created };
178
+ });
179
+ return respond(TaskDataSchema, `Created ${taskKey(store, task)} "${task.title}" [${task.status}, ${task.priority}, ${task.type}]${parent ? ` under ${taskKey(store, parent)}` : ""} (id ${task.id}).`, { task: taskData(store, store.getTask(task.id)) });
180
+ },
181
+ },
182
+ {
183
+ method: "PATCH",
184
+ path: "/tasks/:ref",
185
+ handle: ({ store }, { agent, params, body }) => {
186
+ const input = parseInput(UpdateTaskBodySchema, body);
187
+ const { title, description, status, priority, type, parent, after_task, tested_task_id } = input;
188
+ const { task, summary } = actFor(store, agent, input.request_id, (tx, space) => {
189
+ if ([title, description, status, priority, type, parent, after_task, tested_task_id].every((value) => value === undefined)) {
190
+ throw new OrganizerError("Nothing to change: give at least one of title, description, status, priority, type, parent, after_task or tested_task_id.");
191
+ }
192
+ const patch = {};
193
+ if (title !== undefined)
194
+ patch.title = requireText(title, "title");
195
+ if (description !== undefined)
196
+ patch.description = description;
197
+ if (status !== undefined)
198
+ patch.status = status;
199
+ if (priority !== undefined)
200
+ patch.priority = priority;
201
+ if (type !== undefined)
202
+ patch.type = type;
203
+ const task = resolveTask(tx.store, space, params.ref);
204
+ if (parent !== undefined)
205
+ patch.parentId = parent === null ? null : resolveTask(tx.store, space, parent).id;
206
+ if (tested_task_id !== undefined)
207
+ patch.testedTaskId = tested_task_id === null ? null : resolveTask(tx.store, space, tested_task_id).id;
208
+ const afterId = after_task === undefined ? undefined : after_task === null ? null : resolveTask(tx.store, space, after_task).id;
209
+ const before = tx.store.taskSnapshot(task.id);
210
+ const updated = Object.keys(patch).length > 0 ? tx.updateTask(task.id, patch) : task;
211
+ if (afterId !== undefined)
212
+ tx.moveTask(task.id, updated.status, afterId);
213
+ const after = tx.store.taskSnapshot(task.id);
214
+ const { summary, operation } = taskUpdateSummary(tx.store, before, after);
215
+ tx.recordAgentAction({ entityType: "task", entityId: task.id, operation, before, after, summary, taskId: task.id });
216
+ return { task, summary };
217
+ });
218
+ const updated = store.getTask(task.id);
219
+ return respond(TaskDataSchema, `Updated ${taskKey(store, updated)} "${updated.title}" [${updated.status}, ${updated.priority}, ${updated.type}]: ${summary}.`, {
220
+ task: taskData(store, updated),
221
+ });
222
+ },
223
+ },
224
+ {
225
+ method: "PUT",
226
+ path: "/tasks/:ref/assignees",
227
+ handle: ({ store }, { agent, params, body }) => {
228
+ const input = parseInput(SetAssigneesBodySchema, body);
229
+ const { task, users } = actFor(store, agent, input.request_id, (tx, space) => {
230
+ const task = resolveTask(tx.store, space, params.ref);
231
+ const users = resolveMembers(tx.store, space, input.assignees);
232
+ const before = tx.store.taskSnapshot(task.id);
233
+ tx.setAssignees(task.id, users.map((user) => user.id));
234
+ const after = tx.store.taskSnapshot(task.id);
235
+ tx.recordAgentAction({
236
+ entityType: "task",
237
+ entityId: task.id,
238
+ operation: "assign",
239
+ before,
240
+ after,
241
+ summary: assigneesSummary(tx.store, before.assigneeIds, after.assigneeIds, task.type === "test"),
242
+ taskId: task.id,
243
+ });
244
+ return { task, users };
245
+ });
246
+ const names = users.map(userLabel);
247
+ return respond(TaskDataSchema, `${taskKey(store, task)} is now assigned to ${names.length > 0 ? names.join(", ") : "nobody"}.`, { task: taskData(store, store.getTask(task.id)) });
248
+ },
249
+ },
250
+ {
251
+ method: "PUT",
252
+ path: "/tasks/:ref/tags",
253
+ handle: ({ store }, { agent, params, body }) => {
254
+ const input = parseInput(SetTagsBodySchema, body);
255
+ const { task, tags } = actFor(store, agent, input.request_id, (tx, space) => {
256
+ const task = resolveTask(tx.store, space, params.ref);
257
+ const before = tx.store.taskSnapshot(task.id);
258
+ const tags = tx.setTags(task.id, input.tags);
259
+ tx.recordAgentAction({
260
+ entityType: "task",
261
+ entityId: task.id,
262
+ operation: "update",
263
+ before,
264
+ after: tx.store.taskSnapshot(task.id),
265
+ summary: tags.length > 0 ? `Tags : ${tags.map((tag) => tag.name).join(", ")}` : "Tags retirés",
266
+ taskId: task.id,
267
+ });
268
+ return { task, tags };
269
+ });
270
+ return respond(TaskDataSchema, `${taskKey(store, task)} tags: ${tags.length > 0 ? tags.map((tag) => tag.name).join(", ") : "none"}.`, {
271
+ task: taskData(store, store.getTask(task.id)),
272
+ });
273
+ },
274
+ },
275
+ {
276
+ method: "POST",
277
+ path: "/tasks/:ref/comments",
278
+ handle: ({ store }, { agent, params, body }) => {
279
+ const input = parseInput(AddCommentBodySchema, body);
280
+ const text = requireText(input.body, "body");
281
+ const { task, comment } = actFor(store, agent, input.request_id, (tx, space) => {
282
+ const task = resolveTask(tx.store, space, params.ref);
283
+ const comment = tx.addComment(task.id, text);
284
+ tx.recordAgentAction({ entityType: "comment", entityId: comment.id, operation: "create", before: null, after: comment, summary: "Commentaire ajouté", taskId: task.id });
285
+ return { task, comment };
286
+ });
287
+ return respond(CommentDataSchema, `Commented on ${taskKey(store, task)} (comment id ${comment.id}).`, { task: taskData(store, task), commentId: comment.id });
288
+ },
289
+ },
290
+ ];
@@ -0,0 +1,41 @@
1
+ import type { z } from "zod";
2
+ import type { AgentCaller } from "../core/agent.js";
3
+ import type { Store } from "../core/store.js";
4
+ /** Dépendances de l'API agent. */
5
+ export interface AgentApiContext {
6
+ store: Store;
7
+ /** Horloge, imposée par les tests. */
8
+ now?: () => Date;
9
+ }
10
+ /** Requête `/api/agent/*`, telle que le serveur web la transmet au routeur. */
11
+ export interface AgentApiRequest {
12
+ method: string;
13
+ /** Chemin sous `/api/agent`, par exemple `/requests`. */
14
+ path: string;
15
+ query: Record<string, string>;
16
+ body?: unknown;
17
+ /** Jeton de l'en-tête `Authorization: Bearer`, null sans en-tête. */
18
+ bearer: string | null;
19
+ }
20
+ /** Réponse réussie : texte rédigé pour l'agent et données structurées. */
21
+ export interface AgentApiResponse {
22
+ text: string;
23
+ data: unknown;
24
+ }
25
+ /** Appel d'une route : agent authentifié, paramètres de chemin, requête et corps. */
26
+ export interface AgentCall {
27
+ agent: AgentCaller;
28
+ params: Record<string, string>;
29
+ query: Record<string, string>;
30
+ body: unknown;
31
+ }
32
+ export interface AgentRoute {
33
+ method: "GET" | "POST" | "PUT" | "PATCH";
34
+ /** Chemin sous `/api/agent`, avec `:nom` pour un paramètre. */
35
+ path: string;
36
+ handle: (ctx: AgentApiContext, call: AgentCall) => AgentApiResponse;
37
+ }
38
+ /** Données validées par `schema` ; une donnée invalide est une erreur qui liste les problèmes. */
39
+ export declare function parseInput<S extends z.ZodType>(schema: S, value: unknown): z.infer<S>;
40
+ /** Réponse dont les données sont validées par `schema`. */
41
+ export declare function respond<S extends z.ZodType>(schema: S, text: string, data: z.infer<S>): AgentApiResponse;
@@ -0,0 +1,14 @@
1
+ import { OrganizerError } from "../core/errors.js";
2
+ /** Données validées par `schema` ; une donnée invalide est une erreur qui liste les problèmes. */
3
+ export function parseInput(schema, value) {
4
+ const parsed = schema.safeParse(value ?? {});
5
+ if (!parsed.success) {
6
+ const issues = parsed.error.issues.map((issue) => `${issue.path.map(String).join(".") || "arguments"}: ${issue.message}`);
7
+ throw new OrganizerError(`Invalid arguments — ${issues.join("; ")}. Check the tool's input schema.`);
8
+ }
9
+ return parsed.data;
10
+ }
11
+ /** Réponse dont les données sont validées par `schema`. */
12
+ export function respond(schema, text, data) {
13
+ return { text, data: schema.parse(data) };
14
+ }
@@ -0,0 +1,12 @@
1
+ /** Suffixe du dossier des pièces jointes, à côté du fichier de la base. */
2
+ export declare const ATTACHMENTS_BACKUP_SUFFIX = ".attachments";
3
+ /** Résultat d'une sauvegarde. */
4
+ export interface BackupResult {
5
+ databaseFile: string;
6
+ attachmentsDir: string;
7
+ attachmentCount: number;
8
+ }
9
+ /** Sauvegarde la base de `dataDir` dans `target`, et ses pièces jointes à côté. */
10
+ export declare function backupSite(dataDir: string, target: string, force: boolean): Promise<BackupResult>;
11
+ /** Exécute la commande ; renvoie le code de sortie du processus. */
12
+ export declare function runBackup(argv: readonly string[], dataDir: string): Promise<number>;
@@ -0,0 +1,126 @@
1
+ import Database from "better-sqlite3";
2
+ import fs from "node:fs";
3
+ import path from "node:path";
4
+ import { ATTACHMENTS_DIRNAME } from "../core/attachments.js";
5
+ import { BUSY_TIMEOUT_MS, databasePath } from "../core/db.js";
6
+ /**
7
+ * Commande `mcp-organizer --backup <fichier> [--force]` : sauvegarde du site,
8
+ * en service ou non.
9
+ *
10
+ * La base est copiée par l'API de sauvegarde de SQLite, qui produit un
11
+ * instantané cohérent même pendant les écritures du site. Les pièces jointes
12
+ * référencées par cet instantané sont ensuite copiées dans le dossier
13
+ * `<fichier>.attachments/`, sous leur nom de stockage. Une pièce jointe
14
+ * référencée mais introuvable fait échouer la sauvegarde, qui ne laisse alors
15
+ * rien derrière elle. Une destination existante est refusée, sauf `--force`
16
+ * qui la remplace. Hors Windows, le fichier de la base est créé avec les
17
+ * droits 600 et le dossier des pièces jointes avec les droits 700.
18
+ */
19
+ const USAGE = "Usage : mcp-organizer --backup <fichier> [--force]";
20
+ /** Suffixe du dossier des pièces jointes, à côté du fichier de la base. */
21
+ export const ATTACHMENTS_BACKUP_SUFFIX = ".attachments";
22
+ /** Droits POSIX appliqués aux fichiers de la sauvegarde ; Windows ne les connaît pas. */
23
+ const PRIVATE_MODES = process.platform !== "win32";
24
+ function parseArgs(argv) {
25
+ const rest = argv.slice(argv.indexOf("--backup") + 1);
26
+ const unknown = rest.filter((arg) => arg.startsWith("-") && arg !== "--force");
27
+ if (unknown.length > 0)
28
+ throw new Error(`Option inconnue : ${unknown.join(", ")}.\n${USAGE}`);
29
+ const positional = rest.filter((arg) => !arg.startsWith("-"));
30
+ if (positional.length === 0)
31
+ throw new Error(`--backup attend le fichier de sauvegarde à créer.\n${USAGE}`);
32
+ if (positional.length > 1)
33
+ throw new Error(`--backup attend un seul fichier (reçu : ${positional.join(", ")}).\n${USAGE}`);
34
+ return { target: path.resolve(positional[0]), force: rest.includes("--force") };
35
+ }
36
+ function megabytes(bytes) {
37
+ return `${(bytes / (1024 * 1024)).toFixed(1).replace(".", ",")} Mo`;
38
+ }
39
+ /** Sauvegarde la base de `dataDir` dans `target`, et ses pièces jointes à côté. */
40
+ export async function backupSite(dataDir, target, force) {
41
+ const source = databasePath(dataDir);
42
+ if (!fs.existsSync(source))
43
+ throw new Error(`Aucune base à sauvegarder : ${source} n'existe pas. Vérifiez le répertoire de données (MCP_ORGANIZER_DATA_DIR, ou --dotenv vers le .env du site).`);
44
+ const attachmentsDir = `${target}${ATTACHMENTS_BACKUP_SUFFIX}`;
45
+ if (path.resolve(target) === path.resolve(source))
46
+ throw new Error("La destination est la base elle-même : choisissez un autre fichier.");
47
+ const existing = [target, attachmentsDir].filter((entry) => fs.existsSync(entry));
48
+ if (existing.length > 0 && !force)
49
+ throw new Error(`La destination existe déjà : ${existing.join(", ")}. Choisissez un autre fichier, ou ajoutez --force pour la remplacer.`);
50
+ fs.mkdirSync(path.dirname(target), { recursive: true });
51
+ const partial = `${target}.partial`;
52
+ const partialAttachments = `${attachmentsDir}.partial`;
53
+ fs.rmSync(partial, { force: true });
54
+ fs.rmSync(partialAttachments, { recursive: true, force: true });
55
+ try {
56
+ // La copie contient les empreintes des mots de passe et des jetons : lisible par son seul propriétaire dès sa création.
57
+ fs.writeFileSync(partial, "", { mode: 0o600, flag: "wx" });
58
+ if (PRIVATE_MODES)
59
+ fs.chmodSync(partial, 0o600);
60
+ const db = new Database(source, { fileMustExist: true });
61
+ try {
62
+ db.pragma(`busy_timeout = ${BUSY_TIMEOUT_MS}`);
63
+ await db.backup(partial);
64
+ }
65
+ finally {
66
+ db.close();
67
+ }
68
+ // La copie devient un fichier autonome, sans journal WAL à côté.
69
+ const copy = new Database(partial, { fileMustExist: true });
70
+ let storagePaths;
71
+ try {
72
+ copy.pragma("journal_mode = DELETE");
73
+ const integrity = copy.pragma("integrity_check", { simple: true });
74
+ if (integrity !== "ok")
75
+ throw new Error(`La copie de la base est corrompue (integrity_check : ${String(integrity)}).`);
76
+ storagePaths = copy.prepare("SELECT storage_path FROM attachments ORDER BY storage_path").all().map((row) => row.storage_path);
77
+ }
78
+ finally {
79
+ copy.close();
80
+ }
81
+ fs.mkdirSync(partialAttachments, { mode: 0o700 });
82
+ if (PRIVATE_MODES)
83
+ fs.chmodSync(partialAttachments, 0o700);
84
+ const attachmentsRoot = path.resolve(dataDir, ATTACHMENTS_DIRNAME) + path.sep;
85
+ const missing = [];
86
+ for (const storagePath of storagePaths) {
87
+ const file = path.resolve(dataDir, storagePath);
88
+ if (!file.startsWith(attachmentsRoot))
89
+ throw new Error(`Pièce jointe hors du dossier ${ATTACHMENTS_DIRNAME}/ : ${storagePath}.`);
90
+ if (!fs.existsSync(file)) {
91
+ missing.push(storagePath);
92
+ continue;
93
+ }
94
+ fs.copyFileSync(file, path.join(partialAttachments, path.relative(attachmentsRoot, file)));
95
+ }
96
+ if (missing.length > 0) {
97
+ throw new Error(`${missing.length} pièce(s) jointe(s) référencée(s) par la base sont introuvables dans ${attachmentsRoot} (${missing.slice(0, 5).join(", ")}${missing.length > 5 ? ", …" : ""}). Relancez la sauvegarde ; si l'erreur persiste, ces fichiers ont disparu du répertoire de données.`);
98
+ }
99
+ if (force) {
100
+ fs.rmSync(target, { force: true });
101
+ fs.rmSync(attachmentsDir, { recursive: true, force: true });
102
+ }
103
+ fs.renameSync(partial, target);
104
+ fs.renameSync(partialAttachments, attachmentsDir);
105
+ return { databaseFile: target, attachmentsDir, attachmentCount: storagePaths.length };
106
+ }
107
+ catch (err) {
108
+ fs.rmSync(partial, { force: true });
109
+ fs.rmSync(partialAttachments, { recursive: true, force: true });
110
+ throw err;
111
+ }
112
+ }
113
+ /** Exécute la commande ; renvoie le code de sortie du processus. */
114
+ export async function runBackup(argv, dataDir) {
115
+ try {
116
+ const args = parseArgs(argv);
117
+ const result = await backupSite(dataDir, args.target, args.force);
118
+ process.stdout.write(`Base sauvegardée : ${result.databaseFile} (${megabytes(fs.statSync(result.databaseFile).size)}).\n` +
119
+ `Pièces jointes : ${result.attachmentCount} fichier(s) dans ${result.attachmentsDir}.\n`);
120
+ return 0;
121
+ }
122
+ catch (err) {
123
+ process.stderr.write(`Sauvegarde interrompue : ${err?.message ?? err}\n`);
124
+ return 1;
125
+ }
126
+ }
@@ -0,0 +1,7 @@
1
+ /** Saisies au terminal des commandes interactives (`--set-user`, `--setup`). */
2
+ /** Lit une réponse sans écho : l'interface de lecture écrit dans une sortie qui jette tout. */
3
+ export declare function askSecret(question: string): Promise<string>;
4
+ /** Pose une question en clair et renvoie la réponse sans espaces de bord. */
5
+ export declare function askLine(question: string): Promise<string>;
6
+ /** Pose une question en clair jusqu'à obtenir une valeur non vide. */
7
+ export declare function ask(question: string): Promise<string>;
@@ -0,0 +1,40 @@
1
+ import { Writable } from "node:stream";
2
+ import { createInterface } from "node:readline/promises";
3
+ /** Saisies au terminal des commandes interactives (`--set-user`, `--setup`). */
4
+ /** Lit une réponse sans écho : l'interface de lecture écrit dans une sortie qui jette tout. */
5
+ export async function askSecret(question) {
6
+ process.stdout.write(` ${question} : `);
7
+ const sink = new Writable({
8
+ write(_chunk, _encoding, callback) {
9
+ callback();
10
+ },
11
+ });
12
+ const rl = createInterface({ input: process.stdin, output: sink, terminal: true });
13
+ try {
14
+ const answer = await rl.question("");
15
+ process.stdout.write("\n");
16
+ return answer;
17
+ }
18
+ finally {
19
+ rl.close();
20
+ }
21
+ }
22
+ /** Pose une question en clair et renvoie la réponse sans espaces de bord. */
23
+ export async function askLine(question) {
24
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
25
+ try {
26
+ return (await rl.question(` ${question} : `)).trim();
27
+ }
28
+ finally {
29
+ rl.close();
30
+ }
31
+ }
32
+ /** Pose une question en clair jusqu'à obtenir une valeur non vide. */
33
+ export async function ask(question) {
34
+ for (;;) {
35
+ const value = await askLine(question);
36
+ if (value !== "")
37
+ return value;
38
+ process.stderr.write(" ! Une valeur est attendue.\n");
39
+ }
40
+ }
@@ -0,0 +1,2 @@
1
+ /** Exécute la commande ; renvoie le code de sortie du processus. */
2
+ export declare function runSetUser(argv: readonly string[], dataDir: string): Promise<number>;