@imenam/mcp-organizer 0.0.0-stage → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (165) hide show
  1. package/README.md +548 -3
  2. package/dist/shared/logic/first-account.d.ts +13 -0
  3. package/dist/shared/logic/first-account.js +15 -0
  4. package/dist/shared/logic/labels.d.ts +5 -0
  5. package/dist/shared/logic/labels.js +17 -0
  6. package/dist/shared/logic/positions.d.ts +9 -0
  7. package/dist/shared/logic/positions.js +19 -0
  8. package/dist/shared/logic/spaces.d.ts +13 -0
  9. package/dist/shared/logic/spaces.js +48 -0
  10. package/dist/shared/logic/test-summary.d.ts +28 -0
  11. package/dist/shared/logic/test-summary.js +35 -0
  12. package/dist/shared/schemas/agent-api.schema.d.ts +269 -0
  13. package/dist/shared/schemas/agent-api.schema.js +97 -0
  14. package/dist/shared/schemas/agent-tools.schema.d.ts +818 -0
  15. package/dist/shared/schemas/agent-tools.schema.js +198 -0
  16. package/dist/shared/schemas/api.schema.d.ts +249 -0
  17. package/dist/shared/schemas/api.schema.js +116 -0
  18. package/dist/shared/schemas/entities.schema.d.ts +472 -0
  19. package/dist/shared/schemas/entities.schema.js +262 -0
  20. package/dist/shared/schemas/enums.schema.d.ts +135 -0
  21. package/dist/shared/schemas/enums.schema.js +58 -0
  22. package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
  23. package/dist/shared/schemas/gui-api.schema.js +20 -0
  24. package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
  25. package/dist/shared/schemas/ideas-api.schema.js +40 -0
  26. package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
  27. package/dist/shared/schemas/notifications-api.schema.js +58 -0
  28. package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
  29. package/dist/shared/schemas/tasks-api.schema.js +225 -0
  30. package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
  31. package/dist/shared/schemas/tokens-api.schema.js +42 -0
  32. package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
  33. package/dist/shared/schemas/transcription-api.schema.js +12 -0
  34. package/dist/src/agent-api/checklist.d.ts +3 -0
  35. package/dist/src/agent-api/checklist.js +127 -0
  36. package/dist/src/agent-api/ideas.d.ts +3 -0
  37. package/dist/src/agent-api/ideas.js +93 -0
  38. package/dist/src/agent-api/notifications.d.ts +16 -0
  39. package/dist/src/agent-api/notifications.js +91 -0
  40. package/dist/src/agent-api/requests.d.ts +3 -0
  41. package/dist/src/agent-api/requests.js +60 -0
  42. package/dist/src/agent-api/router.d.ts +3 -0
  43. package/dist/src/agent-api/router.js +41 -0
  44. package/dist/src/agent-api/tasks.d.ts +2 -0
  45. package/dist/src/agent-api/tasks.js +290 -0
  46. package/dist/src/agent-api/types.d.ts +41 -0
  47. package/dist/src/agent-api/types.js +14 -0
  48. package/dist/src/cli/backup.d.ts +12 -0
  49. package/dist/src/cli/backup.js +126 -0
  50. package/dist/src/cli/prompt.d.ts +7 -0
  51. package/dist/src/cli/prompt.js +40 -0
  52. package/dist/src/cli/set-user.d.ts +2 -0
  53. package/dist/src/cli/set-user.js +114 -0
  54. package/dist/src/cli/setup-mcp.d.ts +10 -0
  55. package/dist/src/cli/setup-mcp.js +110 -0
  56. package/dist/src/cli/setup.d.ts +39 -0
  57. package/dist/src/cli/setup.js +626 -0
  58. package/dist/src/cli/watch.d.ts +4 -0
  59. package/dist/src/cli/watch.js +62 -0
  60. package/dist/src/core/accounts.d.ts +129 -0
  61. package/dist/src/core/accounts.js +284 -0
  62. package/dist/src/core/agent-tokens.d.ts +31 -0
  63. package/dist/src/core/agent-tokens.js +120 -0
  64. package/dist/src/core/agent.d.ts +71 -0
  65. package/dist/src/core/agent.js +358 -0
  66. package/dist/src/core/attachments.d.ts +21 -0
  67. package/dist/src/core/attachments.js +44 -0
  68. package/dist/src/core/data-dir.d.ts +5 -0
  69. package/dist/src/core/data-dir.js +11 -0
  70. package/dist/src/core/db.d.ts +13 -0
  71. package/dist/src/core/db.js +38 -0
  72. package/dist/src/core/env-file.d.ts +11 -0
  73. package/dist/src/core/env-file.js +36 -0
  74. package/dist/src/core/errors.d.ts +24 -0
  75. package/dist/src/core/errors.js +42 -0
  76. package/dist/src/core/history.d.ts +34 -0
  77. package/dist/src/core/history.js +279 -0
  78. package/dist/src/core/ideas.d.ts +16 -0
  79. package/dist/src/core/ideas.js +100 -0
  80. package/dist/src/core/ids.d.ts +2 -0
  81. package/dist/src/core/ids.js +5 -0
  82. package/dist/src/core/log-dir.d.ts +2 -0
  83. package/dist/src/core/log-dir.js +9 -0
  84. package/dist/src/core/logger.d.ts +14 -0
  85. package/dist/src/core/logger.js +37 -0
  86. package/dist/src/core/mail.d.ts +43 -0
  87. package/dist/src/core/mail.js +44 -0
  88. package/dist/src/core/mcp-entry.d.ts +20 -0
  89. package/dist/src/core/mcp-entry.js +15 -0
  90. package/dist/src/core/migrations.d.ts +18 -0
  91. package/dist/src/core/migrations.js +404 -0
  92. package/dist/src/core/native-sqlite.d.ts +2 -0
  93. package/dist/src/core/native-sqlite.js +23 -0
  94. package/dist/src/core/notifications.d.ts +46 -0
  95. package/dist/src/core/notifications.js +173 -0
  96. package/dist/src/core/project-root.d.ts +2 -0
  97. package/dist/src/core/project-root.js +13 -0
  98. package/dist/src/core/requests.d.ts +50 -0
  99. package/dist/src/core/requests.js +240 -0
  100. package/dist/src/core/snapshots.d.ts +32 -0
  101. package/dist/src/core/snapshots.js +35 -0
  102. package/dist/src/core/spaces.d.ts +66 -0
  103. package/dist/src/core/spaces.js +252 -0
  104. package/dist/src/core/store.d.ts +577 -0
  105. package/dist/src/core/store.js +2119 -0
  106. package/dist/src/core/task-history.d.ts +7 -0
  107. package/dist/src/core/task-history.js +41 -0
  108. package/dist/src/core/tasks.d.ts +58 -0
  109. package/dist/src/core/tasks.js +389 -0
  110. package/dist/src/core/test-tasks.d.ts +19 -0
  111. package/dist/src/core/test-tasks.js +135 -0
  112. package/dist/src/core/transcription.d.ts +45 -0
  113. package/dist/src/core/transcription.js +92 -0
  114. package/dist/src/core/watchers.d.ts +30 -0
  115. package/dist/src/core/watchers.js +50 -0
  116. package/dist/src/deploy/env-render.d.ts +26 -0
  117. package/dist/src/deploy/env-render.js +98 -0
  118. package/dist/src/deploy/templates.d.ts +38 -0
  119. package/dist/src/deploy/templates.js +130 -0
  120. package/dist/src/gui-api.d.ts +37 -0
  121. package/dist/src/gui-api.js +383 -0
  122. package/dist/src/index.d.ts +2 -0
  123. package/dist/src/index.js +110 -0
  124. package/dist/src/mcp/client.d.ts +38 -0
  125. package/dist/src/mcp/client.js +106 -0
  126. package/dist/src/mcp/notifications.d.ts +13 -0
  127. package/dist/src/mcp/notifications.js +54 -0
  128. package/dist/src/mcp/start.d.ts +14 -0
  129. package/dist/src/mcp/start.js +123 -0
  130. package/dist/src/mcp/tools.d.ts +46 -0
  131. package/dist/src/mcp/tools.js +348 -0
  132. package/dist/src/mcp/watch-handoff.d.ts +35 -0
  133. package/dist/src/mcp/watch-handoff.js +91 -0
  134. package/dist/src/server/app.d.ts +25 -0
  135. package/dist/src/server/app.js +305 -0
  136. package/dist/src/server/config.d.ts +23 -0
  137. package/dist/src/server/config.js +75 -0
  138. package/dist/src/server/limits.d.ts +2 -0
  139. package/dist/src/server/limits.js +4 -0
  140. package/dist/src/server/login-limiter.d.ts +36 -0
  141. package/dist/src/server/login-limiter.js +81 -0
  142. package/dist/src/server/start.d.ts +5 -0
  143. package/dist/src/server/start.js +88 -0
  144. package/dist/src/version.d.ts +1 -0
  145. package/dist/src/version.js +16 -0
  146. package/gui/dist/assets/index-BVN0agCc.css +1 -0
  147. package/gui/dist/assets/index-DI1_kr81.js +70 -0
  148. package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
  149. package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
  150. package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
  151. package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
  152. package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
  153. package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
  154. package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
  155. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
  156. package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
  157. package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
  158. package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
  159. package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
  160. package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
  161. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
  162. package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
  163. package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
  164. package/gui/dist/index.html +13 -0
  165. package/package.json +66 -4
@@ -0,0 +1,91 @@
1
+ import { MAX_REQUEST_LIST_LIMIT, NoQuerySchema, NotificationsDataSchema, WaitRequestsDataSchema, WaitRequestsQuerySchema, } from "../../shared/schemas/agent-tools.schema.js";
2
+ import { resolveAgentToken } from "../core/agent-tokens.js";
3
+ import { requestData, requestLine, spaceData } from "../core/agent.js";
4
+ import { parseInput, respond } from "./types.js";
5
+ /**
6
+ * Notifications de l'agent : état (`GET notifications`) et attente longue
7
+ * des demandes qui passent en attente (`GET requests/wait`), appelée en
8
+ * boucle par le veilleur que lance `enable_notifications`.
9
+ */
10
+ export const notificationRoutes = [
11
+ {
12
+ method: "GET",
13
+ path: "/notifications",
14
+ handle: ({ store }, { agent, query }) => {
15
+ parseInput(NoQuerySchema, query);
16
+ const watcher = store.watchers.status(agent.token.id);
17
+ const pendingCount = store.countPendingRequestsOfSpace(agent.space.id);
18
+ const state = watcher.armed ? `a watcher is armed for this token since ${watcher.since}` : "no watcher is armed for this token";
19
+ return respond(NotificationsDataSchema, `Space "${agent.space.name}": ${pendingCount} pending request(s); ${state}.`, {
20
+ token: { id: agent.token.id, name: agent.token.name },
21
+ space: spaceData(agent.space),
22
+ pendingCount,
23
+ watcher,
24
+ });
25
+ },
26
+ },
27
+ ];
28
+ /** Demandes en attente de l'espace du jeton modifiées strictement après `since`. */
29
+ function pendingSince(ctx, agent, since) {
30
+ return ctx.store.listAgentRequestsTargeting(agent.space.id, "pending", MAX_REQUEST_LIST_LIMIT).filter((request) => request.updatedAt > since);
31
+ }
32
+ function waitResponse(ctx, agent, requests, cursor, timeoutSeconds) {
33
+ const data = {
34
+ space: spaceData(agent.space),
35
+ requests: requests.map((request) => requestData(ctx.store, agent, request)),
36
+ cursor,
37
+ timedOut: requests.length === 0,
38
+ };
39
+ if (requests.length === 0)
40
+ return respond(WaitRequestsDataSchema, `No new pending request in "${agent.space.name}" after ${timeoutSeconds}s.`, data);
41
+ const lines = requests.map((request) => requestLine(ctx.store, agent, request)).join("\n");
42
+ return respond(WaitRequestsDataSchema, `${requests.length} new pending request(s) in "${agent.space.name}":\n${lines}\n\nNext: call claim_request with the id of the oldest request.`, data);
43
+ }
44
+ /**
45
+ * `GET requests/wait` : rend aussitôt les demandes passées en attente depuis
46
+ * `since`, sinon attend qu'une demande de l'espace du jeton passe en attente
47
+ * (création ou libération), au plus `timeout_seconds`. Le curseur rendu est
48
+ * l'horodatage de la dernière demande rendue, ou le point de départ de
49
+ * l'attente à l'expiration. Une attente `watcher=true` compte dans la
50
+ * présence du veilleur du jeton.
51
+ */
52
+ export function waitForPendingRequests(ctx, request, signal) {
53
+ const agent = resolveAgentToken(ctx.store, request.bearer, ctx.now?.() ?? new Date());
54
+ const input = parseInput(WaitRequestsQuerySchema, request.query);
55
+ const startedAt = new Date().toISOString();
56
+ const since = input.since ?? null;
57
+ if (since !== null) {
58
+ const ready = pendingSince(ctx, agent, since);
59
+ if (ready.length > 0)
60
+ return Promise.resolve(waitResponse(ctx, agent, ready, ready.map((r) => r.updatedAt).sort().at(-1), input.timeout_seconds));
61
+ }
62
+ return new Promise((resolve) => {
63
+ const releaseWatcher = input.watcher ? ctx.store.watchers.begin(agent.token.id) : () => { };
64
+ const received = [];
65
+ let flush = null;
66
+ const stop = () => {
67
+ clearTimeout(timer);
68
+ if (flush)
69
+ clearImmediate(flush);
70
+ unsubscribe();
71
+ signal.removeEventListener("abort", onAbort);
72
+ releaseWatcher();
73
+ };
74
+ const finish = (requests, cursor) => {
75
+ stop();
76
+ resolve(waitResponse(ctx, agent, requests, cursor, input.timeout_seconds));
77
+ };
78
+ // Les demandes d'une même mutation arrivent ensemble : elles partent dans la même réponse.
79
+ const unsubscribe = ctx.store.onPendingRequest((pending) => {
80
+ if (pending.targetSpaceId !== agent.space.id || pending.status !== "pending")
81
+ return;
82
+ received.push(pending);
83
+ flush ??= setImmediate(() => finish(received, received.map((r) => r.updatedAt).sort().at(-1)));
84
+ });
85
+ const timer = setTimeout(() => finish([], since ?? startedAt), input.timeout_seconds * 1000);
86
+ const onAbort = () => stop();
87
+ signal.addEventListener("abort", onAbort);
88
+ if (signal.aborted)
89
+ stop();
90
+ });
91
+ }
@@ -0,0 +1,3 @@
1
+ import { type AgentRoute } from "./types.js";
2
+ /** Demandes de l'espace du jeton : liste, prise, fin (réussie ou en échec). */
3
+ export declare const requestRoutes: AgentRoute[];
@@ -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,41 @@
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 { notificationRoutes } from "./notifications.js";
6
+ import { requestRoutes } from "./requests.js";
7
+ import { taskRoutes } from "./tasks.js";
8
+ function compile(route) {
9
+ const keys = [];
10
+ const source = route.path.replace(/:([A-Za-z]+)/g, (_, key) => {
11
+ keys.push(key);
12
+ return "([^/]+)";
13
+ });
14
+ return { route, pattern: new RegExp(`^${source}$`), keys };
15
+ }
16
+ const ROUTES = [...requestRoutes, ...taskRoutes, ...checklistRoutes, ...ideaRoutes, ...notificationRoutes].map(compile);
17
+ /** Segment de chemin décodé ; un encodage `%` invalide est une erreur 400. */
18
+ function decodeSegment(raw) {
19
+ try {
20
+ return decodeURIComponent(raw);
21
+ }
22
+ catch {
23
+ throw new OrganizerError(`Invalid URL: a path segment is badly encoded (${raw}).`);
24
+ }
25
+ }
26
+ /** Traite une requête de l'API agent ; une erreur est levée telle quelle, avec la nature qui fixe son code HTTP. */
27
+ export function handleAgentRequest(ctx, request) {
28
+ const now = ctx.now?.() ?? new Date();
29
+ const agent = resolveAgentToken(ctx.store, request.bearer, now);
30
+ for (const candidate of ROUTES) {
31
+ if (candidate.route.method !== request.method)
32
+ continue;
33
+ const match = candidate.pattern.exec(request.path);
34
+ if (!match)
35
+ continue;
36
+ const params = {};
37
+ candidate.keys.forEach((key, index) => (params[key] = decodeSegment(match[index + 1])));
38
+ return candidate.route.handle(ctx, { agent, params, query: request.query, body: request.body });
39
+ }
40
+ 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.`);
41
+ }
@@ -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>;