@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.
- package/README.md +548 -3
- package/dist/shared/logic/first-account.d.ts +13 -0
- package/dist/shared/logic/first-account.js +15 -0
- package/dist/shared/logic/labels.d.ts +5 -0
- package/dist/shared/logic/labels.js +17 -0
- package/dist/shared/logic/positions.d.ts +9 -0
- package/dist/shared/logic/positions.js +19 -0
- package/dist/shared/logic/spaces.d.ts +13 -0
- package/dist/shared/logic/spaces.js +48 -0
- package/dist/shared/logic/test-summary.d.ts +28 -0
- package/dist/shared/logic/test-summary.js +35 -0
- package/dist/shared/schemas/agent-api.schema.d.ts +269 -0
- package/dist/shared/schemas/agent-api.schema.js +97 -0
- package/dist/shared/schemas/agent-tools.schema.d.ts +818 -0
- package/dist/shared/schemas/agent-tools.schema.js +198 -0
- package/dist/shared/schemas/api.schema.d.ts +249 -0
- package/dist/shared/schemas/api.schema.js +116 -0
- package/dist/shared/schemas/entities.schema.d.ts +472 -0
- package/dist/shared/schemas/entities.schema.js +262 -0
- package/dist/shared/schemas/enums.schema.d.ts +135 -0
- package/dist/shared/schemas/enums.schema.js +58 -0
- package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
- package/dist/shared/schemas/gui-api.schema.js +20 -0
- package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
- package/dist/shared/schemas/ideas-api.schema.js +40 -0
- package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
- package/dist/shared/schemas/notifications-api.schema.js +58 -0
- package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
- package/dist/shared/schemas/tasks-api.schema.js +225 -0
- package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
- package/dist/shared/schemas/tokens-api.schema.js +42 -0
- package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
- package/dist/shared/schemas/transcription-api.schema.js +12 -0
- package/dist/src/agent-api/checklist.d.ts +3 -0
- package/dist/src/agent-api/checklist.js +127 -0
- package/dist/src/agent-api/ideas.d.ts +3 -0
- package/dist/src/agent-api/ideas.js +93 -0
- package/dist/src/agent-api/notifications.d.ts +16 -0
- package/dist/src/agent-api/notifications.js +91 -0
- package/dist/src/agent-api/requests.d.ts +3 -0
- package/dist/src/agent-api/requests.js +60 -0
- package/dist/src/agent-api/router.d.ts +3 -0
- package/dist/src/agent-api/router.js +41 -0
- package/dist/src/agent-api/tasks.d.ts +2 -0
- package/dist/src/agent-api/tasks.js +290 -0
- package/dist/src/agent-api/types.d.ts +41 -0
- package/dist/src/agent-api/types.js +14 -0
- package/dist/src/cli/backup.d.ts +12 -0
- package/dist/src/cli/backup.js +126 -0
- package/dist/src/cli/prompt.d.ts +7 -0
- package/dist/src/cli/prompt.js +40 -0
- package/dist/src/cli/set-user.d.ts +2 -0
- package/dist/src/cli/set-user.js +114 -0
- package/dist/src/cli/setup-mcp.d.ts +10 -0
- package/dist/src/cli/setup-mcp.js +110 -0
- package/dist/src/cli/setup.d.ts +39 -0
- package/dist/src/cli/setup.js +626 -0
- package/dist/src/cli/watch.d.ts +4 -0
- package/dist/src/cli/watch.js +62 -0
- package/dist/src/core/accounts.d.ts +129 -0
- package/dist/src/core/accounts.js +284 -0
- package/dist/src/core/agent-tokens.d.ts +31 -0
- package/dist/src/core/agent-tokens.js +120 -0
- package/dist/src/core/agent.d.ts +71 -0
- package/dist/src/core/agent.js +358 -0
- package/dist/src/core/attachments.d.ts +21 -0
- package/dist/src/core/attachments.js +44 -0
- package/dist/src/core/data-dir.d.ts +5 -0
- package/dist/src/core/data-dir.js +11 -0
- package/dist/src/core/db.d.ts +13 -0
- package/dist/src/core/db.js +38 -0
- package/dist/src/core/env-file.d.ts +11 -0
- package/dist/src/core/env-file.js +36 -0
- package/dist/src/core/errors.d.ts +24 -0
- package/dist/src/core/errors.js +42 -0
- package/dist/src/core/history.d.ts +34 -0
- package/dist/src/core/history.js +279 -0
- package/dist/src/core/ideas.d.ts +16 -0
- package/dist/src/core/ideas.js +100 -0
- package/dist/src/core/ids.d.ts +2 -0
- package/dist/src/core/ids.js +5 -0
- package/dist/src/core/log-dir.d.ts +2 -0
- package/dist/src/core/log-dir.js +9 -0
- package/dist/src/core/logger.d.ts +14 -0
- package/dist/src/core/logger.js +37 -0
- package/dist/src/core/mail.d.ts +43 -0
- package/dist/src/core/mail.js +44 -0
- package/dist/src/core/mcp-entry.d.ts +20 -0
- package/dist/src/core/mcp-entry.js +15 -0
- package/dist/src/core/migrations.d.ts +18 -0
- package/dist/src/core/migrations.js +404 -0
- package/dist/src/core/native-sqlite.d.ts +2 -0
- package/dist/src/core/native-sqlite.js +23 -0
- package/dist/src/core/notifications.d.ts +46 -0
- package/dist/src/core/notifications.js +173 -0
- package/dist/src/core/project-root.d.ts +2 -0
- package/dist/src/core/project-root.js +13 -0
- package/dist/src/core/requests.d.ts +50 -0
- package/dist/src/core/requests.js +240 -0
- package/dist/src/core/snapshots.d.ts +32 -0
- package/dist/src/core/snapshots.js +35 -0
- package/dist/src/core/spaces.d.ts +66 -0
- package/dist/src/core/spaces.js +252 -0
- package/dist/src/core/store.d.ts +577 -0
- package/dist/src/core/store.js +2119 -0
- package/dist/src/core/task-history.d.ts +7 -0
- package/dist/src/core/task-history.js +41 -0
- package/dist/src/core/tasks.d.ts +58 -0
- package/dist/src/core/tasks.js +389 -0
- package/dist/src/core/test-tasks.d.ts +19 -0
- package/dist/src/core/test-tasks.js +135 -0
- package/dist/src/core/transcription.d.ts +45 -0
- package/dist/src/core/transcription.js +92 -0
- package/dist/src/core/watchers.d.ts +30 -0
- package/dist/src/core/watchers.js +50 -0
- package/dist/src/deploy/env-render.d.ts +26 -0
- package/dist/src/deploy/env-render.js +98 -0
- package/dist/src/deploy/templates.d.ts +38 -0
- package/dist/src/deploy/templates.js +130 -0
- package/dist/src/gui-api.d.ts +37 -0
- package/dist/src/gui-api.js +383 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +110 -0
- package/dist/src/mcp/client.d.ts +38 -0
- package/dist/src/mcp/client.js +106 -0
- package/dist/src/mcp/notifications.d.ts +13 -0
- package/dist/src/mcp/notifications.js +54 -0
- package/dist/src/mcp/start.d.ts +14 -0
- package/dist/src/mcp/start.js +123 -0
- package/dist/src/mcp/tools.d.ts +46 -0
- package/dist/src/mcp/tools.js +348 -0
- package/dist/src/mcp/watch-handoff.d.ts +35 -0
- package/dist/src/mcp/watch-handoff.js +91 -0
- package/dist/src/server/app.d.ts +25 -0
- package/dist/src/server/app.js +305 -0
- package/dist/src/server/config.d.ts +23 -0
- package/dist/src/server/config.js +75 -0
- package/dist/src/server/limits.d.ts +2 -0
- package/dist/src/server/limits.js +4 -0
- package/dist/src/server/login-limiter.d.ts +36 -0
- package/dist/src/server/login-limiter.js +81 -0
- package/dist/src/server/start.d.ts +5 -0
- package/dist/src/server/start.js +88 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +16 -0
- package/gui/dist/assets/index-BVN0agCc.css +1 -0
- package/gui/dist/assets/index-DI1_kr81.js +70 -0
- package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/gui/dist/index.html +13 -0
- package/package.json +66 -4
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import type { Actor, Mutation, Store } from "./store.js";
|
|
2
|
+
import type { AgentRequest, Idea, Space, Task, TaskSnapshot, User } from "../../shared/schemas/entities.schema.js";
|
|
3
|
+
import type { AgentIdea, AgentMember, AgentRequestSummary, AgentSpace, AgentTaskSummary } from "../../shared/schemas/agent-tools.schema.js";
|
|
4
|
+
import type { ResolvedAgentToken } from "./agent-tokens.js";
|
|
5
|
+
/**
|
|
6
|
+
* Logique de l'API agent : agent appelant (jeton résolu), demande pour
|
|
7
|
+
* laquelle il agit, résolution des tâches et des membres dans l'espace du
|
|
8
|
+
* jeton, contexte textuel et structuré d'une demande, et résumés français
|
|
9
|
+
* des actions, affichés dans la GUI.
|
|
10
|
+
*/
|
|
11
|
+
/** Agent qui appelle l'API : son jeton, le propriétaire du jeton et l'espace couvert. */
|
|
12
|
+
export type AgentCaller = ResolvedAgentToken;
|
|
13
|
+
export declare function agentActor(agent: AgentCaller, requestId: string): Actor;
|
|
14
|
+
/**
|
|
15
|
+
* Exécute `fn` dans une mutation de l'agent pour la demande `requestId`. La
|
|
16
|
+
* demande et tout ce que `fn` lit sont vérifiés et lus dans la transaction
|
|
17
|
+
* de l'écriture.
|
|
18
|
+
*/
|
|
19
|
+
export declare function actFor<T>(store: Store, agent: AgentCaller, requestId: string, fn: (tx: Mutation, space: Space) => T): T;
|
|
20
|
+
/** Demande de l'espace du jeton ; une demande d'un autre espace est introuvable pour l'agent. */
|
|
21
|
+
export declare function requestOfSpace(store: Store, agent: AgentCaller, requestId: string): AgentRequest;
|
|
22
|
+
/**
|
|
23
|
+
* Demande pour laquelle l'agent veut écrire : elle vise l'espace du jeton,
|
|
24
|
+
* elle est `processing`, prise par ce jeton, et le propriétaire du jeton est
|
|
25
|
+
* encore membre de l'espace. Vérifié à chaque appel.
|
|
26
|
+
*/
|
|
27
|
+
export declare function actingRequest(store: Store, agent: AgentCaller, requestId: string): {
|
|
28
|
+
request: AgentRequest;
|
|
29
|
+
space: Space;
|
|
30
|
+
};
|
|
31
|
+
export declare function userLabel(user: User): string;
|
|
32
|
+
export declare function taskKey(store: Store, task: Task): string;
|
|
33
|
+
/** Tâche de l'espace désignée par son identifiant ou sa clé (`ORG-41`). */
|
|
34
|
+
export declare function resolveTask(store: Store, space: Space, ref: string): Task;
|
|
35
|
+
/** Idée de l'espace. */
|
|
36
|
+
export declare function resolveIdea(store: Store, space: Space, ideaId: string): Idea;
|
|
37
|
+
/** Membres de l'espace désignés par leur identifiant ou leur adresse e-mail. */
|
|
38
|
+
export declare function resolveMembers(store: Store, space: Space, refs: readonly string[]): User[];
|
|
39
|
+
/** Arborescence de l'espace : chaque tâche puis ses sous-tâches, par colonne et rang. */
|
|
40
|
+
export declare function spaceTree(store: Store, space: Space): string[];
|
|
41
|
+
/** Membres de l'espace : nom, adresse, rôle et identifiant (pour set_assignees). */
|
|
42
|
+
export declare function memberLines(store: Store, space: Space): string[];
|
|
43
|
+
/** Durée écoulée depuis `iso`, en anglais. */
|
|
44
|
+
export declare function ago(iso: string, now?: Date): string;
|
|
45
|
+
/** Résumé d'une demande pour list_requests. */
|
|
46
|
+
export declare function requestLine(store: Store, agent: AgentCaller, request: AgentRequest): string;
|
|
47
|
+
/** Contexte complet d'une demande, renvoyé par claim_request. */
|
|
48
|
+
export declare function requestContext(store: Store, agent: AgentCaller, request: AgentRequest): string;
|
|
49
|
+
/** Idées actives récentes de la boîte à idées d'un espace. */
|
|
50
|
+
export declare function recentIdeas(store: Store, space: Space): Idea[];
|
|
51
|
+
/** Arbre des tâches, membres et idées récentes d'un espace, pour claim_request et get_context. */
|
|
52
|
+
export declare function spaceContextLines(store: Store, space: Space): string[];
|
|
53
|
+
export declare function spaceData(space: Space): AgentSpace;
|
|
54
|
+
export declare function memberData(store: Store, space: Space): AgentMember[];
|
|
55
|
+
export declare function taskData(store: Store, task: Task): AgentTaskSummary;
|
|
56
|
+
/** Tâches d'un espace dans l'ordre de l'arbre : chaque tâche puis ses sous-tâches, par colonne et rang. */
|
|
57
|
+
export declare function treeData(store: Store, space: Space): AgentTaskSummary[];
|
|
58
|
+
export declare function ideaData(store: Store, idea: Idea): AgentIdea;
|
|
59
|
+
export declare function requestData(store: Store, agent: AgentCaller, request: AgentRequest): AgentRequestSummary;
|
|
60
|
+
export declare function quote(text: string, max?: number): string;
|
|
61
|
+
/** Résumé d'une création de tâche. */
|
|
62
|
+
export declare function createdTaskSummary(store: Store, task: Task): string;
|
|
63
|
+
/** Résumé des champs changés d'une tâche, et nature de l'opération. */
|
|
64
|
+
export declare function taskUpdateSummary(store: Store, before: TaskSnapshot, after: TaskSnapshot): {
|
|
65
|
+
summary: string;
|
|
66
|
+
operation: "update" | "move";
|
|
67
|
+
};
|
|
68
|
+
/** Résumé d'un changement d'assignés. */
|
|
69
|
+
export declare function assigneesSummary(store: Store, before: readonly string[], after: readonly string[], test: boolean): string;
|
|
70
|
+
/** Vérifie qu'un texte libre n'est pas vide ; le message dit quoi fournir. */
|
|
71
|
+
export declare function requireText(value: string, what: string): string;
|
|
@@ -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,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
|
+
}
|