@imenam/mcp-organizer 0.0.0-stage → 1.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +529 -2
- package/dist/shared/logic/first-account.d.ts +13 -0
- package/dist/shared/logic/first-account.js +15 -0
- package/dist/shared/logic/labels.d.ts +5 -0
- package/dist/shared/logic/labels.js +17 -0
- package/dist/shared/logic/positions.d.ts +9 -0
- package/dist/shared/logic/positions.js +19 -0
- package/dist/shared/logic/spaces.d.ts +13 -0
- package/dist/shared/logic/spaces.js +48 -0
- package/dist/shared/logic/test-summary.d.ts +28 -0
- package/dist/shared/logic/test-summary.js +35 -0
- package/dist/shared/schemas/agent-api.schema.d.ts +268 -0
- package/dist/shared/schemas/agent-api.schema.js +94 -0
- package/dist/shared/schemas/agent-tools.schema.d.ts +724 -0
- package/dist/shared/schemas/agent-tools.schema.js +165 -0
- package/dist/shared/schemas/api.schema.d.ts +249 -0
- package/dist/shared/schemas/api.schema.js +116 -0
- package/dist/shared/schemas/entities.schema.d.ts +472 -0
- package/dist/shared/schemas/entities.schema.js +262 -0
- package/dist/shared/schemas/enums.schema.d.ts +135 -0
- package/dist/shared/schemas/enums.schema.js +58 -0
- package/dist/shared/schemas/gui-api.schema.d.ts +53 -0
- package/dist/shared/schemas/gui-api.schema.js +20 -0
- package/dist/shared/schemas/ideas-api.schema.d.ts +81 -0
- package/dist/shared/schemas/ideas-api.schema.js +40 -0
- package/dist/shared/schemas/notifications-api.schema.d.ts +87 -0
- package/dist/shared/schemas/notifications-api.schema.js +58 -0
- package/dist/shared/schemas/tasks-api.schema.d.ts +675 -0
- package/dist/shared/schemas/tasks-api.schema.js +225 -0
- package/dist/shared/schemas/tokens-api.schema.d.ts +106 -0
- package/dist/shared/schemas/tokens-api.schema.js +42 -0
- package/dist/shared/schemas/transcription-api.schema.d.ts +15 -0
- package/dist/shared/schemas/transcription-api.schema.js +12 -0
- package/dist/src/agent-api/checklist.d.ts +3 -0
- package/dist/src/agent-api/checklist.js +127 -0
- package/dist/src/agent-api/ideas.d.ts +3 -0
- package/dist/src/agent-api/ideas.js +93 -0
- package/dist/src/agent-api/requests.d.ts +3 -0
- package/dist/src/agent-api/requests.js +60 -0
- package/dist/src/agent-api/router.d.ts +3 -0
- package/dist/src/agent-api/router.js +40 -0
- package/dist/src/agent-api/tasks.d.ts +2 -0
- package/dist/src/agent-api/tasks.js +290 -0
- package/dist/src/agent-api/types.d.ts +41 -0
- package/dist/src/agent-api/types.js +14 -0
- package/dist/src/cli/backup.d.ts +12 -0
- package/dist/src/cli/backup.js +126 -0
- package/dist/src/cli/prompt.d.ts +7 -0
- package/dist/src/cli/prompt.js +40 -0
- package/dist/src/cli/set-user.d.ts +2 -0
- package/dist/src/cli/set-user.js +114 -0
- package/dist/src/cli/setup-mcp.d.ts +10 -0
- package/dist/src/cli/setup-mcp.js +110 -0
- package/dist/src/cli/setup.d.ts +39 -0
- package/dist/src/cli/setup.js +626 -0
- package/dist/src/core/accounts.d.ts +129 -0
- package/dist/src/core/accounts.js +284 -0
- package/dist/src/core/agent-tokens.d.ts +31 -0
- package/dist/src/core/agent-tokens.js +120 -0
- package/dist/src/core/agent.d.ts +71 -0
- package/dist/src/core/agent.js +358 -0
- package/dist/src/core/attachments.d.ts +21 -0
- package/dist/src/core/attachments.js +44 -0
- package/dist/src/core/data-dir.d.ts +5 -0
- package/dist/src/core/data-dir.js +11 -0
- package/dist/src/core/db.d.ts +13 -0
- package/dist/src/core/db.js +38 -0
- package/dist/src/core/env-file.d.ts +11 -0
- package/dist/src/core/env-file.js +36 -0
- package/dist/src/core/errors.d.ts +24 -0
- package/dist/src/core/errors.js +42 -0
- package/dist/src/core/history.d.ts +34 -0
- package/dist/src/core/history.js +279 -0
- package/dist/src/core/ideas.d.ts +16 -0
- package/dist/src/core/ideas.js +100 -0
- package/dist/src/core/ids.d.ts +2 -0
- package/dist/src/core/ids.js +5 -0
- package/dist/src/core/log-dir.d.ts +2 -0
- package/dist/src/core/log-dir.js +9 -0
- package/dist/src/core/logger.d.ts +14 -0
- package/dist/src/core/logger.js +37 -0
- package/dist/src/core/mail.d.ts +43 -0
- package/dist/src/core/mail.js +44 -0
- package/dist/src/core/mcp-entry.d.ts +20 -0
- package/dist/src/core/mcp-entry.js +15 -0
- package/dist/src/core/migrations.d.ts +18 -0
- package/dist/src/core/migrations.js +404 -0
- package/dist/src/core/native-sqlite.d.ts +2 -0
- package/dist/src/core/native-sqlite.js +23 -0
- package/dist/src/core/notifications.d.ts +46 -0
- package/dist/src/core/notifications.js +173 -0
- package/dist/src/core/project-root.d.ts +2 -0
- package/dist/src/core/project-root.js +13 -0
- package/dist/src/core/requests.d.ts +49 -0
- package/dist/src/core/requests.js +239 -0
- package/dist/src/core/snapshots.d.ts +32 -0
- package/dist/src/core/snapshots.js +35 -0
- package/dist/src/core/spaces.d.ts +66 -0
- package/dist/src/core/spaces.js +252 -0
- package/dist/src/core/store.d.ts +565 -0
- package/dist/src/core/store.js +2093 -0
- package/dist/src/core/task-history.d.ts +7 -0
- package/dist/src/core/task-history.js +41 -0
- package/dist/src/core/tasks.d.ts +58 -0
- package/dist/src/core/tasks.js +389 -0
- package/dist/src/core/test-tasks.d.ts +19 -0
- package/dist/src/core/test-tasks.js +135 -0
- package/dist/src/core/transcription.d.ts +45 -0
- package/dist/src/core/transcription.js +92 -0
- package/dist/src/deploy/env-render.d.ts +26 -0
- package/dist/src/deploy/env-render.js +98 -0
- package/dist/src/deploy/templates.d.ts +38 -0
- package/dist/src/deploy/templates.js +130 -0
- package/dist/src/gui-api.d.ts +37 -0
- package/dist/src/gui-api.js +383 -0
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +100 -0
- package/dist/src/mcp/client.d.ts +23 -0
- package/dist/src/mcp/client.js +81 -0
- package/dist/src/mcp/start.d.ts +14 -0
- package/dist/src/mcp/start.js +104 -0
- package/dist/src/mcp/tools.d.ts +46 -0
- package/dist/src/mcp/tools.js +348 -0
- package/dist/src/server/app.d.ts +25 -0
- package/dist/src/server/app.js +290 -0
- package/dist/src/server/config.d.ts +23 -0
- package/dist/src/server/config.js +75 -0
- package/dist/src/server/limits.d.ts +2 -0
- package/dist/src/server/limits.js +4 -0
- package/dist/src/server/login-limiter.d.ts +36 -0
- package/dist/src/server/login-limiter.js +81 -0
- package/dist/src/server/start.d.ts +5 -0
- package/dist/src/server/start.js +88 -0
- package/dist/src/version.d.ts +1 -0
- package/dist/src/version.js +16 -0
- package/gui/dist/assets/index-DwkXx4Q2.js +70 -0
- package/gui/dist/assets/index-lnvnkDdg.css +1 -0
- package/gui/dist/assets/inter-cyrillic-ext-wght-normal-BOeWTOD4.woff2 +0 -0
- package/gui/dist/assets/inter-cyrillic-wght-normal-DqGufNeO.woff2 +0 -0
- package/gui/dist/assets/inter-greek-ext-wght-normal-DlzME5K_.woff2 +0 -0
- package/gui/dist/assets/inter-greek-wght-normal-CkhJZR-_.woff2 +0 -0
- package/gui/dist/assets/inter-latin-ext-wght-normal-DO1Apj_S.woff2 +0 -0
- package/gui/dist/assets/inter-latin-wght-normal-Dx4kXJAl.woff2 +0 -0
- package/gui/dist/assets/inter-vietnamese-wght-normal-CBcvBZtf.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-BEIGL1Tu.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-cyrillic-400-normal-ugxPyKxw.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-B9oWc5Lo.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-greek-400-normal-C190GLew.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-6-qcROiO.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-400-normal-V6pRDFza.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-Bc8Ftmh3.woff2 +0 -0
- package/gui/dist/assets/jetbrains-mono-latin-ext-400-normal-fXTG6kC5.woff +0 -0
- package/gui/dist/assets/jetbrains-mono-vietnamese-400-normal-CqNFfHCs.woff +0 -0
- package/gui/dist/index.html +13 -0
- package/package.json +66 -4
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Migrations SQL versionnées. La migration d'indice i amène la base à la
|
|
3
|
+
* version i + 1 (`PRAGMA user_version`). Une migration publiée ne se modifie
|
|
4
|
+
* plus : toute évolution du schéma s'ajoute en fin de liste.
|
|
5
|
+
*/
|
|
6
|
+
export const MIGRATIONS = [
|
|
7
|
+
`
|
|
8
|
+
CREATE TABLE users (
|
|
9
|
+
id TEXT PRIMARY KEY,
|
|
10
|
+
email TEXT NOT NULL UNIQUE CHECK (email = lower(email) AND email <> ''),
|
|
11
|
+
display_name TEXT,
|
|
12
|
+
password_hash TEXT,
|
|
13
|
+
status TEXT NOT NULL CHECK (status IN ('invited', 'active', 'disabled')),
|
|
14
|
+
created_at TEXT NOT NULL,
|
|
15
|
+
CHECK (status = 'invited' OR display_name IS NOT NULL)
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE access_links (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
21
|
+
purpose TEXT NOT NULL CHECK (purpose IN ('invite', 'reset')),
|
|
22
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
23
|
+
expires_at TEXT NOT NULL,
|
|
24
|
+
used_at TEXT,
|
|
25
|
+
created_at TEXT NOT NULL
|
|
26
|
+
);
|
|
27
|
+
CREATE INDEX access_links_user ON access_links(user_id);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE sessions (
|
|
30
|
+
id_hash TEXT PRIMARY KEY,
|
|
31
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
32
|
+
created_at TEXT NOT NULL,
|
|
33
|
+
expires_at TEXT NOT NULL
|
|
34
|
+
);
|
|
35
|
+
CREATE INDEX sessions_user ON sessions(user_id);
|
|
36
|
+
CREATE INDEX sessions_expiry ON sessions(expires_at);
|
|
37
|
+
|
|
38
|
+
CREATE TABLE user_settings (
|
|
39
|
+
user_id TEXT PRIMARY KEY REFERENCES users(id) ON DELETE CASCADE,
|
|
40
|
+
agent_scope TEXT NOT NULL DEFAULT 'personal' CHECK (agent_scope IN ('personal', 'current_space')),
|
|
41
|
+
notify_assign_app INTEGER NOT NULL DEFAULT 1 CHECK (notify_assign_app IN (0, 1)),
|
|
42
|
+
notify_assign_email INTEGER NOT NULL DEFAULT 1 CHECK (notify_assign_email IN (0, 1)),
|
|
43
|
+
notify_comment_app INTEGER NOT NULL DEFAULT 1 CHECK (notify_comment_app IN (0, 1)),
|
|
44
|
+
notify_comment_email INTEGER NOT NULL DEFAULT 1 CHECK (notify_comment_email IN (0, 1))
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
CREATE TABLE spaces (
|
|
48
|
+
id TEXT PRIMARY KEY,
|
|
49
|
+
name TEXT NOT NULL CHECK (name <> ''),
|
|
50
|
+
kind TEXT NOT NULL CHECK (kind IN ('personal', 'shared')),
|
|
51
|
+
owner_id TEXT NOT NULL REFERENCES users(id),
|
|
52
|
+
key_prefix TEXT NOT NULL,
|
|
53
|
+
next_task_number INTEGER NOT NULL DEFAULT 1 CHECK (next_task_number >= 1),
|
|
54
|
+
created_at TEXT NOT NULL
|
|
55
|
+
);
|
|
56
|
+
CREATE UNIQUE INDEX spaces_one_personal_per_user ON spaces(owner_id) WHERE kind = 'personal';
|
|
57
|
+
|
|
58
|
+
CREATE TABLE memberships (
|
|
59
|
+
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
60
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
61
|
+
role TEXT NOT NULL CHECK (role IN ('owner', 'member')),
|
|
62
|
+
joined_at TEXT NOT NULL,
|
|
63
|
+
PRIMARY KEY (space_id, user_id)
|
|
64
|
+
);
|
|
65
|
+
CREATE INDEX memberships_user ON memberships(user_id);
|
|
66
|
+
|
|
67
|
+
CREATE TABLE invitations (
|
|
68
|
+
id TEXT PRIMARY KEY,
|
|
69
|
+
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
70
|
+
email TEXT NOT NULL CHECK (email = lower(email)),
|
|
71
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
72
|
+
invited_by TEXT NOT NULL REFERENCES users(id),
|
|
73
|
+
created_at TEXT NOT NULL,
|
|
74
|
+
expires_at TEXT NOT NULL,
|
|
75
|
+
accepted_at TEXT,
|
|
76
|
+
revoked_at TEXT
|
|
77
|
+
);
|
|
78
|
+
CREATE INDEX invitations_space ON invitations(space_id);
|
|
79
|
+
CREATE INDEX invitations_email ON invitations(email);
|
|
80
|
+
|
|
81
|
+
CREATE TABLE tasks (
|
|
82
|
+
id TEXT PRIMARY KEY,
|
|
83
|
+
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
84
|
+
number INTEGER NOT NULL CHECK (number >= 1),
|
|
85
|
+
parent_id TEXT REFERENCES tasks(id) ON DELETE CASCADE,
|
|
86
|
+
depth INTEGER NOT NULL CHECK (depth IN (0, 1, 2)),
|
|
87
|
+
type TEXT NOT NULL CHECK (type IN ('standard', 'test')),
|
|
88
|
+
title TEXT NOT NULL CHECK (title <> ''),
|
|
89
|
+
description TEXT NOT NULL DEFAULT '',
|
|
90
|
+
status TEXT NOT NULL CHECK (status IN ('todo', 'doing', 'review', 'done')),
|
|
91
|
+
priority TEXT NOT NULL CHECK (priority IN ('low', 'medium', 'high', 'critical')),
|
|
92
|
+
position REAL NOT NULL,
|
|
93
|
+
created_by TEXT NOT NULL REFERENCES users(id),
|
|
94
|
+
created_at TEXT NOT NULL,
|
|
95
|
+
updated_at TEXT NOT NULL,
|
|
96
|
+
version INTEGER NOT NULL DEFAULT 1 CHECK (version >= 1),
|
|
97
|
+
UNIQUE (space_id, number),
|
|
98
|
+
CHECK ((parent_id IS NULL) = (depth = 0))
|
|
99
|
+
);
|
|
100
|
+
CREATE INDEX tasks_column ON tasks(space_id, parent_id, status, position);
|
|
101
|
+
CREATE INDEX tasks_parent ON tasks(parent_id);
|
|
102
|
+
|
|
103
|
+
CREATE TABLE task_assignees (
|
|
104
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
105
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
106
|
+
PRIMARY KEY (task_id, user_id)
|
|
107
|
+
);
|
|
108
|
+
CREATE INDEX task_assignees_user ON task_assignees(user_id);
|
|
109
|
+
|
|
110
|
+
CREATE TABLE tags (
|
|
111
|
+
id TEXT PRIMARY KEY,
|
|
112
|
+
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
113
|
+
name TEXT NOT NULL CHECK (name <> ''),
|
|
114
|
+
UNIQUE (space_id, name)
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
CREATE TABLE task_tags (
|
|
118
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
119
|
+
tag_id TEXT NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
|
|
120
|
+
PRIMARY KEY (task_id, tag_id)
|
|
121
|
+
);
|
|
122
|
+
CREATE INDEX task_tags_tag ON task_tags(tag_id);
|
|
123
|
+
|
|
124
|
+
CREATE TABLE checklist_items (
|
|
125
|
+
id TEXT PRIMARY KEY,
|
|
126
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
127
|
+
text TEXT NOT NULL CHECK (text <> ''),
|
|
128
|
+
done INTEGER NOT NULL DEFAULT 0 CHECK (done IN (0, 1)),
|
|
129
|
+
position REAL NOT NULL
|
|
130
|
+
);
|
|
131
|
+
CREATE INDEX checklist_items_task ON checklist_items(task_id, position);
|
|
132
|
+
|
|
133
|
+
CREATE TABLE test_checkpoints (
|
|
134
|
+
id TEXT PRIMARY KEY,
|
|
135
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
136
|
+
text TEXT NOT NULL CHECK (text <> ''),
|
|
137
|
+
position REAL NOT NULL
|
|
138
|
+
);
|
|
139
|
+
CREATE INDEX test_checkpoints_task ON test_checkpoints(task_id, position);
|
|
140
|
+
|
|
141
|
+
CREATE TABLE test_results (
|
|
142
|
+
checkpoint_id TEXT NOT NULL REFERENCES test_checkpoints(id) ON DELETE CASCADE,
|
|
143
|
+
tester_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
144
|
+
result TEXT NOT NULL CHECK (result IN ('ok', 'ko', 'blocked', 'untested')),
|
|
145
|
+
comment TEXT NOT NULL DEFAULT '',
|
|
146
|
+
updated_at TEXT NOT NULL,
|
|
147
|
+
PRIMARY KEY (checkpoint_id, tester_id)
|
|
148
|
+
);
|
|
149
|
+
CREATE INDEX test_results_tester ON test_results(tester_id);
|
|
150
|
+
|
|
151
|
+
CREATE TABLE test_reviews (
|
|
152
|
+
id TEXT PRIMARY KEY,
|
|
153
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
154
|
+
tester_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
155
|
+
text TEXT NOT NULL DEFAULT '',
|
|
156
|
+
submitted_at TEXT,
|
|
157
|
+
request_id TEXT REFERENCES agent_requests(id) ON DELETE SET NULL
|
|
158
|
+
);
|
|
159
|
+
CREATE INDEX test_reviews_task ON test_reviews(task_id, tester_id);
|
|
160
|
+
|
|
161
|
+
CREATE TABLE comments (
|
|
162
|
+
id TEXT PRIMARY KEY,
|
|
163
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
164
|
+
author_id TEXT NOT NULL REFERENCES users(id),
|
|
165
|
+
body TEXT NOT NULL CHECK (body <> ''),
|
|
166
|
+
created_at TEXT NOT NULL,
|
|
167
|
+
edited_at TEXT
|
|
168
|
+
);
|
|
169
|
+
CREATE INDEX comments_task ON comments(task_id, created_at);
|
|
170
|
+
|
|
171
|
+
CREATE TABLE attachments (
|
|
172
|
+
id TEXT PRIMARY KEY,
|
|
173
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
174
|
+
uploader_id TEXT NOT NULL REFERENCES users(id),
|
|
175
|
+
filename TEXT NOT NULL,
|
|
176
|
+
mime TEXT NOT NULL,
|
|
177
|
+
size INTEGER NOT NULL CHECK (size >= 0),
|
|
178
|
+
storage_path TEXT NOT NULL,
|
|
179
|
+
created_at TEXT NOT NULL
|
|
180
|
+
);
|
|
181
|
+
CREATE INDEX attachments_task ON attachments(task_id);
|
|
182
|
+
|
|
183
|
+
CREATE TABLE ideas (
|
|
184
|
+
id TEXT PRIMARY KEY,
|
|
185
|
+
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
186
|
+
author_id TEXT NOT NULL REFERENCES users(id),
|
|
187
|
+
text TEXT NOT NULL CHECK (text <> ''),
|
|
188
|
+
origin TEXT NOT NULL CHECK (origin IN ('manual', 'agent')),
|
|
189
|
+
request_id TEXT REFERENCES agent_requests(id) ON DELETE SET NULL,
|
|
190
|
+
promoted_task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
191
|
+
archived_at TEXT,
|
|
192
|
+
created_at TEXT NOT NULL
|
|
193
|
+
);
|
|
194
|
+
CREATE INDEX ideas_space ON ideas(space_id, created_at);
|
|
195
|
+
|
|
196
|
+
CREATE TABLE agent_requests (
|
|
197
|
+
id TEXT PRIMARY KEY,
|
|
198
|
+
author_id TEXT NOT NULL REFERENCES users(id),
|
|
199
|
+
source_space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
200
|
+
target_space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
201
|
+
kind TEXT NOT NULL CHECK (kind IN ('idea', 'remark', 'test_review', 'message', 'idea_promotion')),
|
|
202
|
+
context_task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL,
|
|
203
|
+
context_idea_id TEXT REFERENCES ideas(id) ON DELETE SET NULL,
|
|
204
|
+
text TEXT NOT NULL CHECK (text <> ''),
|
|
205
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'processing', 'done', 'failed', 'cancelled')),
|
|
206
|
+
reply TEXT,
|
|
207
|
+
failure_reason TEXT,
|
|
208
|
+
created_at TEXT NOT NULL,
|
|
209
|
+
updated_at TEXT NOT NULL,
|
|
210
|
+
claimed_at TEXT,
|
|
211
|
+
completed_at TEXT,
|
|
212
|
+
cancelled_by TEXT REFERENCES users(id)
|
|
213
|
+
);
|
|
214
|
+
CREATE INDEX agent_requests_status ON agent_requests(status, created_at);
|
|
215
|
+
CREATE INDEX agent_requests_source ON agent_requests(source_space_id, created_at);
|
|
216
|
+
CREATE INDEX agent_requests_target ON agent_requests(target_space_id);
|
|
217
|
+
|
|
218
|
+
CREATE TABLE agent_actions (
|
|
219
|
+
id TEXT PRIMARY KEY,
|
|
220
|
+
request_id TEXT NOT NULL REFERENCES agent_requests(id) ON DELETE CASCADE,
|
|
221
|
+
seq INTEGER NOT NULL CHECK (seq >= 1),
|
|
222
|
+
entity_type TEXT NOT NULL,
|
|
223
|
+
entity_id TEXT NOT NULL,
|
|
224
|
+
operation TEXT NOT NULL,
|
|
225
|
+
before TEXT,
|
|
226
|
+
after TEXT,
|
|
227
|
+
summary TEXT NOT NULL,
|
|
228
|
+
created_at TEXT NOT NULL,
|
|
229
|
+
undone_at TEXT,
|
|
230
|
+
undone_by TEXT REFERENCES users(id),
|
|
231
|
+
UNIQUE (request_id, seq)
|
|
232
|
+
);
|
|
233
|
+
CREATE INDEX agent_actions_entity ON agent_actions(entity_type, entity_id);
|
|
234
|
+
|
|
235
|
+
CREATE TABLE entity_versions (
|
|
236
|
+
entity_type TEXT NOT NULL CHECK (entity_type IN ('task', 'idea')),
|
|
237
|
+
entity_id TEXT NOT NULL,
|
|
238
|
+
version INTEGER NOT NULL CHECK (version >= 1),
|
|
239
|
+
snapshot TEXT,
|
|
240
|
+
actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'agent')),
|
|
241
|
+
actor_id TEXT NOT NULL,
|
|
242
|
+
request_id TEXT,
|
|
243
|
+
created_at TEXT NOT NULL,
|
|
244
|
+
PRIMARY KEY (entity_type, entity_id, version)
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
CREATE TABLE notifications (
|
|
248
|
+
id TEXT PRIMARY KEY,
|
|
249
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
250
|
+
type TEXT NOT NULL CHECK (type IN ('assigned', 'comment')),
|
|
251
|
+
task_id TEXT NOT NULL REFERENCES tasks(id) ON DELETE CASCADE,
|
|
252
|
+
actor_type TEXT NOT NULL CHECK (actor_type IN ('user', 'agent')),
|
|
253
|
+
actor_id TEXT,
|
|
254
|
+
request_id TEXT,
|
|
255
|
+
comment_id TEXT REFERENCES comments(id) ON DELETE CASCADE,
|
|
256
|
+
created_at TEXT NOT NULL,
|
|
257
|
+
read_at TEXT,
|
|
258
|
+
emailed_at TEXT,
|
|
259
|
+
email_error TEXT
|
|
260
|
+
);
|
|
261
|
+
CREATE INDEX notifications_user ON notifications(user_id, read_at, created_at);
|
|
262
|
+
|
|
263
|
+
CREATE TABLE changes (
|
|
264
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
265
|
+
space_id TEXT,
|
|
266
|
+
user_id TEXT,
|
|
267
|
+
entity_type TEXT NOT NULL,
|
|
268
|
+
entity_id TEXT NOT NULL,
|
|
269
|
+
at TEXT NOT NULL
|
|
270
|
+
);
|
|
271
|
+
CREATE INDEX changes_space ON changes(space_id, seq);
|
|
272
|
+
CREATE INDEX changes_user ON changes(user_id, seq);
|
|
273
|
+
`,
|
|
274
|
+
// Une invitation n'a pas de jeton propre : le lien envoyé est l'AccessLink du compte invité.
|
|
275
|
+
`
|
|
276
|
+
CREATE TABLE invitations_v2 (
|
|
277
|
+
id TEXT PRIMARY KEY,
|
|
278
|
+
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
279
|
+
email TEXT NOT NULL CHECK (email = lower(email)),
|
|
280
|
+
invited_by TEXT NOT NULL REFERENCES users(id),
|
|
281
|
+
created_at TEXT NOT NULL,
|
|
282
|
+
expires_at TEXT NOT NULL,
|
|
283
|
+
accepted_at TEXT,
|
|
284
|
+
revoked_at TEXT
|
|
285
|
+
);
|
|
286
|
+
INSERT INTO invitations_v2 (id, space_id, email, invited_by, created_at, expires_at, accepted_at, revoked_at)
|
|
287
|
+
SELECT id, space_id, email, invited_by, created_at, expires_at, accepted_at, revoked_at FROM invitations;
|
|
288
|
+
DROP TABLE invitations;
|
|
289
|
+
ALTER TABLE invitations_v2 RENAME TO invitations;
|
|
290
|
+
CREATE INDEX invitations_space ON invitations(space_id);
|
|
291
|
+
CREATE INDEX invitations_email ON invitations(email);
|
|
292
|
+
CREATE UNIQUE INDEX invitations_one_open_per_email ON invitations(space_id, email) WHERE accepted_at IS NULL AND revoked_at IS NULL;
|
|
293
|
+
`,
|
|
294
|
+
// Tâche concernée par un changement : la tâche elle-même, ou celle qui porte l'élément modifié.
|
|
295
|
+
`
|
|
296
|
+
ALTER TABLE changes ADD COLUMN task_id TEXT;
|
|
297
|
+
`,
|
|
298
|
+
// Un brouillon de review au plus par testeur et par tâche.
|
|
299
|
+
`
|
|
300
|
+
CREATE UNIQUE INDEX test_reviews_one_draft ON test_reviews(task_id, tester_id) WHERE submitted_at IS NULL;
|
|
301
|
+
`,
|
|
302
|
+
// Une notification absente de l'application (préférence désactivée) ne sert qu'à suivre l'envoi de son e-mail.
|
|
303
|
+
`
|
|
304
|
+
ALTER TABLE notifications ADD COLUMN in_app INTEGER NOT NULL DEFAULT 1 CHECK (in_app IN (0, 1));
|
|
305
|
+
DROP INDEX notifications_user;
|
|
306
|
+
CREATE INDEX notifications_user ON notifications(user_id, in_app, read_at, created_at);
|
|
307
|
+
CREATE INDEX notifications_comment ON notifications(comment_id);
|
|
308
|
+
`,
|
|
309
|
+
// Auteur de chaque changement, et demande de l'agent à l'origine d'un commentaire.
|
|
310
|
+
`
|
|
311
|
+
ALTER TABLE changes ADD COLUMN actor_type TEXT;
|
|
312
|
+
ALTER TABLE changes ADD COLUMN actor_id TEXT;
|
|
313
|
+
ALTER TABLE changes ADD COLUMN request_id TEXT;
|
|
314
|
+
CREATE INDEX changes_entity ON changes(entity_type, entity_id, seq);
|
|
315
|
+
ALTER TABLE comments ADD COLUMN request_id TEXT REFERENCES agent_requests(id) ON DELETE SET NULL;
|
|
316
|
+
`,
|
|
317
|
+
// Tâche vérifiée par une tâche de test ; les instantanés de tâche déjà enregistrés reçoivent le champ, vide.
|
|
318
|
+
`
|
|
319
|
+
ALTER TABLE tasks ADD COLUMN tested_task_id TEXT REFERENCES tasks(id) ON DELETE SET NULL;
|
|
320
|
+
CREATE INDEX tasks_tested ON tasks(tested_task_id);
|
|
321
|
+
UPDATE entity_versions SET snapshot = json_set(snapshot, '$.testedTaskId', json('null')) WHERE entity_type = 'task' AND snapshot IS NOT NULL;
|
|
322
|
+
UPDATE agent_actions SET before = json_set(before, '$.testedTaskId', json('null')) WHERE entity_type = 'task' AND before IS NOT NULL;
|
|
323
|
+
UPDATE agent_actions SET after = json_set(after, '$.testedTaskId', json('null')) WHERE entity_type = 'task' AND after IS NOT NULL;
|
|
324
|
+
`,
|
|
325
|
+
// Une demande vise l'espace où elle est écrite : le réglage de portée disparaît et chaque demande appartient à l'espace qu'elle vise.
|
|
326
|
+
`
|
|
327
|
+
ALTER TABLE user_settings DROP COLUMN agent_scope;
|
|
328
|
+
UPDATE agent_requests SET source_space_id = target_space_id WHERE source_space_id <> target_space_id;
|
|
329
|
+
`,
|
|
330
|
+
// Un lien d'invitation est rattaché à l'invitation qui l'a émis ; les liens d'invitation non rattachés ne servent plus.
|
|
331
|
+
// Le destinataire d'une invitation peut la refuser.
|
|
332
|
+
`
|
|
333
|
+
ALTER TABLE access_links ADD COLUMN invitation_id TEXT REFERENCES invitations(id) ON DELETE CASCADE;
|
|
334
|
+
CREATE INDEX access_links_invitation ON access_links(invitation_id);
|
|
335
|
+
DELETE FROM access_links WHERE purpose = 'invite' AND used_at IS NULL;
|
|
336
|
+
ALTER TABLE invitations ADD COLUMN declined_at TEXT;
|
|
337
|
+
DROP INDEX invitations_one_open_per_email;
|
|
338
|
+
CREATE UNIQUE INDEX invitations_one_open_per_email ON invitations(space_id, email) WHERE accepted_at IS NULL AND revoked_at IS NULL AND declined_at IS NULL;
|
|
339
|
+
`,
|
|
340
|
+
// Jetons d'agent : un jeton couvre un espace pour un utilisateur ; seul le hash sha256 du jeton est stocké.
|
|
341
|
+
`
|
|
342
|
+
CREATE TABLE agent_tokens (
|
|
343
|
+
id TEXT PRIMARY KEY,
|
|
344
|
+
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
|
345
|
+
space_id TEXT NOT NULL REFERENCES spaces(id) ON DELETE CASCADE,
|
|
346
|
+
name TEXT NOT NULL CHECK (name <> ''),
|
|
347
|
+
prefix TEXT NOT NULL,
|
|
348
|
+
token_hash TEXT NOT NULL UNIQUE,
|
|
349
|
+
created_at TEXT NOT NULL,
|
|
350
|
+
last_used_at TEXT,
|
|
351
|
+
expires_at TEXT,
|
|
352
|
+
revoked_at TEXT
|
|
353
|
+
);
|
|
354
|
+
CREATE INDEX agent_tokens_user ON agent_tokens(user_id, created_at);
|
|
355
|
+
CREATE INDEX agent_tokens_space ON agent_tokens(space_id);
|
|
356
|
+
`,
|
|
357
|
+
// Jeton qui a pris une demande, et jeton auteur de chaque action de l'agent et de chaque version qu'il crée.
|
|
358
|
+
`
|
|
359
|
+
ALTER TABLE agent_requests ADD COLUMN claimed_by_token_id TEXT REFERENCES agent_tokens(id) ON DELETE SET NULL;
|
|
360
|
+
CREATE INDEX agent_requests_claimed_by ON agent_requests(claimed_by_token_id);
|
|
361
|
+
ALTER TABLE agent_actions ADD COLUMN token_id TEXT REFERENCES agent_tokens(id) ON DELETE SET NULL;
|
|
362
|
+
ALTER TABLE entity_versions ADD COLUMN token_id TEXT REFERENCES agent_tokens(id) ON DELETE SET NULL;
|
|
363
|
+
CREATE INDEX agent_requests_target_status ON agent_requests(target_space_id, status, created_at);
|
|
364
|
+
`,
|
|
365
|
+
// Retire l'espace source des demandes, identique à l'espace visé : une demande n'a qu'un espace.
|
|
366
|
+
// L'index (target_space_id, status, created_at) sert aussi les recherches par espace seul.
|
|
367
|
+
`
|
|
368
|
+
DROP INDEX agent_requests_source;
|
|
369
|
+
DROP INDEX agent_requests_target;
|
|
370
|
+
ALTER TABLE agent_requests DROP COLUMN source_space_id;
|
|
371
|
+
`,
|
|
372
|
+
// Révoque les jetons dont le propriétaire n'est plus membre de l'espace couvert : un départ révoque ses jetons.
|
|
373
|
+
`
|
|
374
|
+
UPDATE agent_tokens SET revoked_at = strftime('%Y-%m-%dT%H:%M:%fZ', 'now')
|
|
375
|
+
WHERE revoked_at IS NULL
|
|
376
|
+
AND NOT EXISTS (SELECT 1 FROM memberships m WHERE m.space_id = agent_tokens.space_id AND m.user_id = agent_tokens.user_id);
|
|
377
|
+
`,
|
|
378
|
+
];
|
|
379
|
+
/** Version du schéma attendue par ce code. */
|
|
380
|
+
export const SCHEMA_VERSION = MIGRATIONS.length;
|
|
381
|
+
/** Version courante du schéma de la base. */
|
|
382
|
+
export function schemaVersion(db) {
|
|
383
|
+
return db.pragma("user_version", { simple: true });
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Applique les migrations manquantes, chacune dans une transaction exclusive
|
|
387
|
+
* qui relit la version : deux processus qui migrent la même base en même temps
|
|
388
|
+
* n'appliquent chaque migration qu'une fois. Une base plus récente que le code
|
|
389
|
+
* est refusée.
|
|
390
|
+
*/
|
|
391
|
+
export function migrate(db) {
|
|
392
|
+
const current = schemaVersion(db);
|
|
393
|
+
if (current > SCHEMA_VERSION) {
|
|
394
|
+
throw new Error(`The database schema is version ${current}, newer than the version ${SCHEMA_VERSION} this mcp-organizer supports. Update the @imenam/mcp-organizer package.`);
|
|
395
|
+
}
|
|
396
|
+
for (let index = current; index < SCHEMA_VERSION; index++) {
|
|
397
|
+
db.transaction(() => {
|
|
398
|
+
if (schemaVersion(db) !== index)
|
|
399
|
+
return;
|
|
400
|
+
db.exec(MIGRATIONS[index]);
|
|
401
|
+
db.pragma(`user_version = ${index + 1}`);
|
|
402
|
+
}).immediate();
|
|
403
|
+
}
|
|
404
|
+
}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
export declare const MISSING_SQLITE_BINDINGS_MESSAGE = "better-sqlite3 n'a pas de module natif compil\u00E9 (\u00AB Could not locate the bindings file \u00BB) : ses scripts d'installation n'ont pas \u00E9t\u00E9 ex\u00E9cut\u00E9s, ce qui arrive avec npm 12 pour un paquet install\u00E9 par npm install ou npx.\nLe site et ses commandes (--server, --set-user, --setup, --backup) en ont besoin ; le mode MCP, non.\n- Installez le site par clonage du d\u00E9p\u00F4t : git clone <d\u00E9p\u00F4t>, puis npm ci et npm run build (README, \u00AB Installer le site \u00BB) ;\n- ou, dans cette installation, autorisez et construisez le module : npm install-scripts approve better-sqlite3, puis npm rebuild better-sqlite3.";
|
|
2
|
+
export declare function assertSqliteLoadable(): Promise<void>;
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { ConfigError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Vérifie que better-sqlite3 charge son module natif, en ouvrant une base en
|
|
4
|
+
* mémoire. Sans ce module (scripts d'installation npm non exécutés), la
|
|
5
|
+
* vérification lève une `ConfigError` qui dit comment installer le site.
|
|
6
|
+
*/
|
|
7
|
+
/** Message de better-sqlite3 (paquet `bindings`) quand le module natif n'a pas été construit. */
|
|
8
|
+
const MISSING_BINDINGS = /Could not locate the bindings file/;
|
|
9
|
+
export const MISSING_SQLITE_BINDINGS_MESSAGE = `better-sqlite3 n'a pas de module natif compilé (« Could not locate the bindings file ») : ses scripts d'installation n'ont pas été exécutés, ce qui arrive avec npm 12 pour un paquet installé par npm install ou npx.
|
|
10
|
+
Le site et ses commandes (--server, --set-user, --setup, --backup) en ont besoin ; le mode MCP, non.
|
|
11
|
+
- Installez le site par clonage du dépôt : git clone <dépôt>, puis npm ci et npm run build (README, « Installer le site ») ;
|
|
12
|
+
- ou, dans cette installation, autorisez et construisez le module : npm install-scripts approve better-sqlite3, puis npm rebuild better-sqlite3.`;
|
|
13
|
+
export async function assertSqliteLoadable() {
|
|
14
|
+
const { default: Database } = await import("better-sqlite3");
|
|
15
|
+
try {
|
|
16
|
+
new Database(":memory:").close();
|
|
17
|
+
}
|
|
18
|
+
catch (err) {
|
|
19
|
+
if (MISSING_BINDINGS.test(String(err?.message ?? err)))
|
|
20
|
+
throw new ConfigError(MISSING_SQLITE_BINDINGS_MESSAGE);
|
|
21
|
+
throw err;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { Mailer } from "./mail.js";
|
|
2
|
+
import type { Transcriber } from "./transcription.js";
|
|
3
|
+
import type { Store } from "./store.js";
|
|
4
|
+
import type { NotificationListQuery, NotificationListResponse, SettingsResponse, UnreadCountResponse, UpdateSettingsRequest } from "../../shared/schemas/notifications-api.schema.js";
|
|
5
|
+
/** Dépendances de l'envoi des e-mails de notification. */
|
|
6
|
+
export interface NotificationMailDeps {
|
|
7
|
+
mailer: Mailer;
|
|
8
|
+
/** URL publique de la GUI (`ORGANIZER_PUBLIC_URL`), base du lien vers la tâche. */
|
|
9
|
+
publicUrl: string | undefined;
|
|
10
|
+
/** Journal des erreurs inattendues de l'envoi. */
|
|
11
|
+
logError: (message: string) => void;
|
|
12
|
+
}
|
|
13
|
+
/** Raison pour laquelle les e-mails de notification ne partent pas, ou null s'ils peuvent partir. */
|
|
14
|
+
export declare function notificationEmailBlocker(deps: Pick<NotificationMailDeps, "mailer" | "publicUrl">): string | null;
|
|
15
|
+
/**
|
|
16
|
+
* Envoi des e-mails de notification. Abonné au store, il reçoit les
|
|
17
|
+
* notifications dont le destinataire veut un e-mail après la validation de
|
|
18
|
+
* la mutation qui les a créées, envoie chaque e-mail et enregistre son issue
|
|
19
|
+
* (`emailedAt` ou `emailError`).
|
|
20
|
+
*/
|
|
21
|
+
export declare class NotificationMailer {
|
|
22
|
+
private readonly store;
|
|
23
|
+
private readonly deps;
|
|
24
|
+
private readonly inFlight;
|
|
25
|
+
constructor(store: Store, deps: NotificationMailDeps);
|
|
26
|
+
/** Abonne l'envoi aux mutations du store. */
|
|
27
|
+
attach(): this;
|
|
28
|
+
/** Attend la fin des envois en cours. */
|
|
29
|
+
flush(): Promise<void>;
|
|
30
|
+
private enqueue;
|
|
31
|
+
private deliver;
|
|
32
|
+
private send;
|
|
33
|
+
private message;
|
|
34
|
+
}
|
|
35
|
+
/** Notifications de l'utilisateur, non lues d'abord, par page. */
|
|
36
|
+
export declare function listNotifications(store: Store, userId: string, query: NotificationListQuery): NotificationListResponse;
|
|
37
|
+
export declare function unreadCount(store: Store, userId: string): UnreadCountResponse;
|
|
38
|
+
/** Marque lue une notification de l'utilisateur ; celle d'un autre est introuvable. */
|
|
39
|
+
export declare function markNotificationRead(store: Store, userId: string, notificationId: string): UnreadCountResponse;
|
|
40
|
+
export declare function markAllNotificationsRead(store: Store, userId: string): UnreadCountResponse;
|
|
41
|
+
/** Ce que les paramètres affichent du serveur : e-mail et dictée. */
|
|
42
|
+
export type SettingsDeps = Pick<NotificationMailDeps, "mailer" | "publicUrl"> & {
|
|
43
|
+
transcriber: Pick<Transcriber, "enabled" | "disabledReason">;
|
|
44
|
+
};
|
|
45
|
+
export declare function getSettings(store: Store, deps: SettingsDeps, userId: string): SettingsResponse;
|
|
46
|
+
export declare function updateSettings(store: Store, deps: SettingsDeps, userId: string, patch: UpdateSettingsRequest): SettingsResponse;
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { NotFoundError } from "./errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Notifications (assignation, commentaire) et préférences. Les notifications
|
|
4
|
+
* sont créées par le store dans la transaction de l'événement ; leurs e-mails
|
|
5
|
+
* partent après la validation, et l'issue de chaque envoi est enregistrée.
|
|
6
|
+
*/
|
|
7
|
+
const asUser = (userId) => ({ type: "user", userId });
|
|
8
|
+
/** Longueur maximale de l'extrait d'un commentaire. */
|
|
9
|
+
const EXCERPT_LENGTH = 120;
|
|
10
|
+
/** Nom de l'agent dans les phrases des notifications. */
|
|
11
|
+
const AGENT_NAME = "L'agent";
|
|
12
|
+
function publicBase(publicUrl) {
|
|
13
|
+
return publicUrl?.trim().replace(/\/+$/, "") || null;
|
|
14
|
+
}
|
|
15
|
+
/** Raison pour laquelle les e-mails de notification ne partent pas, ou null s'ils peuvent partir. */
|
|
16
|
+
export function notificationEmailBlocker(deps) {
|
|
17
|
+
if (!deps.mailer.enabled)
|
|
18
|
+
return deps.mailer.disabledReason;
|
|
19
|
+
if (!publicBase(deps.publicUrl)) {
|
|
20
|
+
return "E-mail non envoyé : ORGANIZER_PUBLIC_URL n'est pas défini dans la configuration du site, le lien vers la tâche ne peut pas figurer dans le message.";
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** Première ligne d'un texte, abrégée. */
|
|
25
|
+
function excerpt(text) {
|
|
26
|
+
const line = text.trim().split(/\r?\n/)[0].trim();
|
|
27
|
+
return line.length > EXCERPT_LENGTH ? `${line.slice(0, EXCERPT_LENGTH - 1)}…` : line;
|
|
28
|
+
}
|
|
29
|
+
function actorName(store, notification) {
|
|
30
|
+
if (notification.actorType === "agent" || notification.actorId === null)
|
|
31
|
+
return null;
|
|
32
|
+
const actor = store.findUser(notification.actorId);
|
|
33
|
+
return actor ? (actor.displayName ?? actor.email) : null;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Envoi des e-mails de notification. Abonné au store, il reçoit les
|
|
37
|
+
* notifications dont le destinataire veut un e-mail après la validation de
|
|
38
|
+
* la mutation qui les a créées, envoie chaque e-mail et enregistre son issue
|
|
39
|
+
* (`emailedAt` ou `emailError`).
|
|
40
|
+
*/
|
|
41
|
+
export class NotificationMailer {
|
|
42
|
+
store;
|
|
43
|
+
deps;
|
|
44
|
+
inFlight = new Set();
|
|
45
|
+
constructor(store, deps) {
|
|
46
|
+
this.store = store;
|
|
47
|
+
this.deps = deps;
|
|
48
|
+
}
|
|
49
|
+
/** Abonne l'envoi aux mutations du store. */
|
|
50
|
+
attach() {
|
|
51
|
+
this.store.setEmailListener((ids) => this.enqueue(ids));
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
/** Attend la fin des envois en cours. */
|
|
55
|
+
async flush() {
|
|
56
|
+
while (this.inFlight.size > 0)
|
|
57
|
+
await Promise.all([...this.inFlight]);
|
|
58
|
+
}
|
|
59
|
+
enqueue(ids) {
|
|
60
|
+
const delivery = this.deliver(ids).finally(() => this.inFlight.delete(delivery));
|
|
61
|
+
this.inFlight.add(delivery);
|
|
62
|
+
}
|
|
63
|
+
async deliver(ids) {
|
|
64
|
+
for (const id of ids) {
|
|
65
|
+
try {
|
|
66
|
+
const notification = this.store.findNotification(id);
|
|
67
|
+
if (!notification)
|
|
68
|
+
continue;
|
|
69
|
+
const outcome = await this.send(notification);
|
|
70
|
+
// La notification disparaît avec sa tâche ou son commentaire, éventuellement supprimés pendant l'envoi.
|
|
71
|
+
if (!this.store.findNotification(id))
|
|
72
|
+
continue;
|
|
73
|
+
this.store.mutate(asUser(notification.userId), (tx) => tx.recordNotificationEmail(id, outcome));
|
|
74
|
+
}
|
|
75
|
+
catch (err) {
|
|
76
|
+
this.deps.logError(`E-mail of notification ${id} failed: ${err?.stack ?? err}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
async send(notification) {
|
|
81
|
+
const blocker = notificationEmailBlocker(this.deps);
|
|
82
|
+
if (blocker)
|
|
83
|
+
return { sent: false, reason: blocker };
|
|
84
|
+
const recipient = this.store.getUser(notification.userId);
|
|
85
|
+
const outcome = await this.deps.mailer.send(this.message(notification, recipient.email));
|
|
86
|
+
return outcome.sent ? { sent: true } : { sent: false, reason: outcome.reason };
|
|
87
|
+
}
|
|
88
|
+
message(notification, to) {
|
|
89
|
+
const task = this.store.getTask(notification.taskId);
|
|
90
|
+
const space = this.store.getSpace(task.spaceId);
|
|
91
|
+
const key = `${space.keyPrefix}-${task.number}`;
|
|
92
|
+
const who = actorName(this.store, notification) ?? AGENT_NAME;
|
|
93
|
+
const url = `${publicBase(this.deps.publicUrl)}/#/task/${encodeURIComponent(task.id)}`;
|
|
94
|
+
const footer = `\n\nOuvrir la tâche : ${url}\n\nCes e-mails se désactivent dans les Paramètres de mcp-organizer.\n`;
|
|
95
|
+
if (notification.type === "assigned") {
|
|
96
|
+
return {
|
|
97
|
+
to,
|
|
98
|
+
subject: `${who} vous a assigné « ${task.title} »`,
|
|
99
|
+
text: `${who} vous a assigné la tâche ${key} « ${task.title} » dans l'espace « ${space.name} ».${footer}`,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
const comment = notification.commentId === null ? null : this.store.findComment(notification.commentId);
|
|
103
|
+
return {
|
|
104
|
+
to,
|
|
105
|
+
subject: `${who} a commenté « ${task.title} »`,
|
|
106
|
+
text: `${who} a commenté la tâche ${key} « ${task.title} » dans l'espace « ${space.name} » :\n\n${comment?.body ?? ""}${footer}`,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
// --- API ------------------------------------------------------------------------
|
|
111
|
+
function notificationView(store, notification) {
|
|
112
|
+
const task = store.getTask(notification.taskId);
|
|
113
|
+
const space = store.getSpace(task.spaceId);
|
|
114
|
+
const comment = notification.commentId === null ? null : store.findComment(notification.commentId);
|
|
115
|
+
return {
|
|
116
|
+
id: notification.id,
|
|
117
|
+
type: notification.type,
|
|
118
|
+
taskId: task.id,
|
|
119
|
+
taskKey: `${space.keyPrefix}-${task.number}`,
|
|
120
|
+
taskTitle: task.title,
|
|
121
|
+
actorType: notification.actorType,
|
|
122
|
+
actorId: notification.actorId,
|
|
123
|
+
actorName: actorName(store, notification),
|
|
124
|
+
commentExcerpt: comment ? excerpt(comment.body) : null,
|
|
125
|
+
createdAt: notification.createdAt,
|
|
126
|
+
readAt: notification.readAt,
|
|
127
|
+
emailedAt: notification.emailedAt,
|
|
128
|
+
emailError: notification.emailError,
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
/** Notifications de l'utilisateur, non lues d'abord, par page. */
|
|
132
|
+
export function listNotifications(store, userId, query) {
|
|
133
|
+
const page = store.listNotifications(userId, query.limit + 1, query.offset);
|
|
134
|
+
return {
|
|
135
|
+
notifications: page.slice(0, query.limit).map((notification) => notificationView(store, notification)),
|
|
136
|
+
unreadCount: store.countUnreadNotifications(userId),
|
|
137
|
+
hasMore: page.length > query.limit,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
export function unreadCount(store, userId) {
|
|
141
|
+
return { unreadCount: store.countUnreadNotifications(userId) };
|
|
142
|
+
}
|
|
143
|
+
/** Marque lue une notification de l'utilisateur ; celle d'un autre est introuvable. */
|
|
144
|
+
export function markNotificationRead(store, userId, notificationId) {
|
|
145
|
+
const notification = store.findNotification(notificationId);
|
|
146
|
+
if (!notification || notification.userId !== userId || !notification.inApp)
|
|
147
|
+
throw new NotFoundError("Notification introuvable.");
|
|
148
|
+
store.mutate(asUser(userId), (tx) => tx.markNotificationRead(notificationId));
|
|
149
|
+
return unreadCount(store, userId);
|
|
150
|
+
}
|
|
151
|
+
export function markAllNotificationsRead(store, userId) {
|
|
152
|
+
store.mutate(asUser(userId), (tx) => tx.markAllNotificationsRead());
|
|
153
|
+
return unreadCount(store, userId);
|
|
154
|
+
}
|
|
155
|
+
function settingsView(store, deps, userId) {
|
|
156
|
+
const { notifyAssignApp, notifyAssignEmail, notifyCommentApp, notifyCommentEmail } = store.getUserSettings(userId);
|
|
157
|
+
const reason = notificationEmailBlocker(deps);
|
|
158
|
+
return {
|
|
159
|
+
notifyAssignApp,
|
|
160
|
+
notifyAssignEmail,
|
|
161
|
+
notifyCommentApp,
|
|
162
|
+
notifyCommentEmail,
|
|
163
|
+
email: { enabled: reason === null, reason },
|
|
164
|
+
transcription: { enabled: deps.transcriber.enabled, reason: deps.transcriber.disabledReason },
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
export function getSettings(store, deps, userId) {
|
|
168
|
+
return settingsView(store, deps, userId);
|
|
169
|
+
}
|
|
170
|
+
export function updateSettings(store, deps, userId, patch) {
|
|
171
|
+
store.mutate(asUser(userId), (tx) => tx.updateUserSettings(userId, patch));
|
|
172
|
+
return settingsView(store, deps, userId);
|
|
173
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
/** Racine du package : premier dossier contenant un package.json en remontant depuis `startDir`. */
|
|
4
|
+
export function findProjectRoot(startDir) {
|
|
5
|
+
let current = startDir;
|
|
6
|
+
while (!fs.existsSync(path.join(current, "package.json"))) {
|
|
7
|
+
const parent = path.dirname(current);
|
|
8
|
+
if (parent === current)
|
|
9
|
+
throw new Error(`No package.json found above ${startDir}: the mcp-organizer package is incomplete, reinstall it.`);
|
|
10
|
+
current = parent;
|
|
11
|
+
}
|
|
12
|
+
return current;
|
|
13
|
+
}
|