@imenam/mcp-organizer 1.0.3 → 1.0.7

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 (45) hide show
  1. package/README.md +71 -20
  2. package/dist/shared/schemas/admin-api.schema.d.ts +101 -0
  3. package/dist/shared/schemas/admin-api.schema.js +39 -0
  4. package/dist/shared/schemas/agent-api.schema.d.ts +2 -0
  5. package/dist/shared/schemas/agent-api.schema.js +4 -0
  6. package/dist/shared/schemas/agent-tools.schema.d.ts +34 -0
  7. package/dist/shared/schemas/agent-tools.schema.js +2 -0
  8. package/dist/shared/schemas/api.schema.d.ts +2 -0
  9. package/dist/shared/schemas/api.schema.js +2 -0
  10. package/dist/shared/schemas/entities.schema.d.ts +27 -1
  11. package/dist/shared/schemas/entities.schema.js +21 -2
  12. package/dist/shared/schemas/notifications-api.schema.d.ts +31 -0
  13. package/dist/shared/schemas/notifications-api.schema.js +22 -1
  14. package/dist/src/agent-api/requests.js +2 -1
  15. package/dist/src/cli/set-admin.d.ts +1 -0
  16. package/dist/src/cli/set-admin.js +45 -0
  17. package/dist/src/core/accounts.js +2 -1
  18. package/dist/src/core/admin.d.ts +24 -0
  19. package/dist/src/core/admin.js +85 -0
  20. package/dist/src/core/agent-tokens.d.ts +9 -1
  21. package/dist/src/core/agent-tokens.js +25 -2
  22. package/dist/src/core/agent.d.ts +5 -0
  23. package/dist/src/core/agent.js +23 -0
  24. package/dist/src/core/migrations.js +369 -344
  25. package/dist/src/core/notifications.d.ts +7 -1
  26. package/dist/src/core/notifications.js +32 -1
  27. package/dist/src/core/requests.d.ts +1 -1
  28. package/dist/src/core/requests.js +4 -4
  29. package/dist/src/core/store.d.ts +38 -2
  30. package/dist/src/core/store.js +161 -38
  31. package/dist/src/gui-api.d.ts +6 -0
  32. package/dist/src/gui-api.js +25 -4
  33. package/dist/src/index.js +8 -1
  34. package/dist/src/mcp/start.d.ts +1 -1
  35. package/dist/src/mcp/start.js +1 -1
  36. package/dist/src/mcp/tools.js +1 -1
  37. package/dist/src/server/runner.d.ts +51 -0
  38. package/dist/src/server/runner.js +222 -0
  39. package/dist/src/server/start.js +8 -0
  40. package/gui/dist/assets/index-DuoSNpBr.js +70 -0
  41. package/gui/dist/assets/index-duxL0fvO.css +1 -0
  42. package/gui/dist/index.html +2 -2
  43. package/package.json +3 -2
  44. package/gui/dist/assets/index-BVN0agCc.css +0 -1
  45. package/gui/dist/assets/index-DI1_kr81.js +0 -70
@@ -4,7 +4,7 @@ import { positionBetween } from "../../shared/logic/positions.js";
4
4
  import { newId } from "./ids.js";
5
5
  import { WatcherRegistry } from "./watchers.js";
6
6
  import { MAX_TASK_DEPTH, } from "../../shared/schemas/enums.schema.js";
7
- import { AccessLinkSchema, AgentActionSchema, AgentTokenSchema, AgentRequestSchema, AttachmentSchema, ChangeSchema, ChecklistItemSchema, CommentSchema, EntityVersionSchema, IdeaSchema, IdeaSnapshotSchema, InvitationSchema, MembershipSchema, NotificationSchema, SessionSchema, SpaceSchema, TagSchema, TaskSchema, TaskSnapshotSchema, TestCheckpointSchema, TestResultSchema, TestReviewSchema, UserSchema, UserSettingsSchema, } from "../../shared/schemas/entities.schema.js";
7
+ import { AccessLinkSchema, AgentActionSchema, AgentTokenSchema, AgentRequestSchema, AttachmentSchema, ChangeSchema, ChecklistItemSchema, CommentSchema, EntityVersionSchema, IdeaSchema, IdeaSnapshotSchema, InvitationSchema, MembershipSchema, NotificationSchema, SessionSchema, SpaceSchema, TagSchema, TaskSchema, TaskSnapshotSchema, TestCheckpointSchema, TestResultSchema, TestReviewSchema, UserSchema, UserSettingsSchema, SpaceNotificationSettingsSchema, } from "../../shared/schemas/entities.schema.js";
8
8
  /** Valide `value` avec `schema` ; une donnée invalide est une erreur métier qui liste les champs fautifs. */
9
9
  function validate(schema, value, what) {
10
10
  const parsed = schema.safeParse(value);
@@ -24,9 +24,25 @@ function userFromRow(row) {
24
24
  displayName: row.display_name,
25
25
  passwordHash: row.password_hash,
26
26
  status: row.status,
27
+ isAdmin: bool(row.is_admin),
27
28
  createdAt: row.created_at,
28
29
  });
29
30
  }
31
+ function spaceNotificationSettingsFromRow(row) {
32
+ return SpaceNotificationSettingsSchema.parse({
33
+ userId: row.user_id,
34
+ spaceId: row.space_id,
35
+ notifyAssignApp: bool(row.notify_assign_app),
36
+ notifyAssignEmail: bool(row.notify_assign_email),
37
+ notifyCommentApp: bool(row.notify_comment_app),
38
+ notifyCommentEmail: bool(row.notify_comment_email),
39
+ });
40
+ }
41
+ /** Les quatre préférences de notification d'un objet qui les porte. */
42
+ function preferencesOf(source) {
43
+ const { notifyAssignApp, notifyAssignEmail, notifyCommentApp, notifyCommentEmail } = source;
44
+ return { notifyAssignApp, notifyAssignEmail, notifyCommentApp, notifyCommentEmail };
45
+ }
30
46
  function settingsFromRow(row) {
31
47
  return UserSettingsSchema.parse({
32
48
  userId: row.user_id,
@@ -34,6 +50,7 @@ function settingsFromRow(row) {
34
50
  notifyAssignEmail: bool(row.notify_assign_email),
35
51
  notifyCommentApp: bool(row.notify_comment_app),
36
52
  notifyCommentEmail: bool(row.notify_comment_email),
53
+ assistantHistory: row.assistant_history,
37
54
  });
38
55
  }
39
56
  function sessionFromRow(row) {
@@ -63,6 +80,7 @@ function agentTokenFromRow(row) {
63
80
  lastUsedAt: row.last_used_at,
64
81
  expiresAt: row.expires_at,
65
82
  revokedAt: row.revoked_at,
83
+ isRunner: bool(row.is_runner),
66
84
  });
67
85
  }
68
86
  function invitationFromRow(row) {
@@ -86,6 +104,7 @@ function spaceFromRow(row) {
86
104
  ownerId: row.owner_id,
87
105
  keyPrefix: row.key_prefix,
88
106
  nextTaskNumber: row.next_task_number,
107
+ runnerAllowed: bool(row.runner_allowed),
89
108
  createdAt: row.created_at,
90
109
  });
91
110
  }
@@ -278,6 +297,7 @@ export class Store {
278
297
  activeMutation = null;
279
298
  emailListener = null;
280
299
  pendingRequestListeners = new Set();
300
+ runnerAllowedListeners = new Set();
281
301
  /** Présence des veilleurs de notifications, par jeton d'agent. */
282
302
  watchers = new WatcherRegistry();
283
303
  constructor(db) {
@@ -308,6 +328,22 @@ export class Store {
308
328
  countUsers() {
309
329
  return this.db.prepare("SELECT COUNT(*) AS n FROM users").get().n;
310
330
  }
331
+ /** Tous les comptes, du plus ancien au plus récent. */
332
+ listUsers() {
333
+ return this.db.prepare("SELECT * FROM users ORDER BY created_at, id").all().map(userFromRow);
334
+ }
335
+ countAdmins() {
336
+ return this.db.prepare("SELECT COUNT(*) AS n FROM users WHERE is_admin = 1").get().n;
337
+ }
338
+ /** Préférences de notification propres à un espace, ou null si l'utilisateur y suit celles de son compte. */
339
+ findSpaceNotificationSettings(userId, spaceId) {
340
+ const row = this.db.prepare("SELECT * FROM space_notification_settings WHERE user_id = ? AND space_id = ?").get(userId, spaceId);
341
+ return row ? spaceNotificationSettingsFromRow(row) : null;
342
+ }
343
+ /** Préférences de notification qui s'appliquent à un utilisateur dans un espace : celles de l'espace, sinon celles du compte. */
344
+ notificationPreferences(userId, spaceId) {
345
+ return preferencesOf(this.findSpaceNotificationSettings(userId, spaceId) ?? this.getUserSettings(userId));
346
+ }
311
347
  getUserSettings(userId) {
312
348
  const row = this.db.prepare("SELECT * FROM user_settings WHERE user_id = ?").get(userId);
313
349
  if (!row)
@@ -338,6 +374,11 @@ export class Store {
338
374
  return row ? agentTokenFromRow(row) : null;
339
375
  }
340
376
  /** Jetons qui couvrent un espace, tous propriétaires confondus, du plus récemment utilisé au moins récemment. */
377
+ /** Jeton du runner de l'espace, s'il a déjà servi. */
378
+ findRunnerToken(spaceId) {
379
+ const row = this.db.prepare("SELECT * FROM agent_tokens WHERE space_id = ? AND is_runner = 1 AND revoked_at IS NULL").get(spaceId);
380
+ return row ? agentTokenFromRow(row) : null;
381
+ }
341
382
  listAgentTokensOfSpace(spaceId) {
342
383
  const rows = this.db
343
384
  .prepare("SELECT * FROM agent_tokens WHERE space_id = ? ORDER BY last_used_at IS NULL, last_used_at DESC, created_at DESC, id DESC")
@@ -392,11 +433,16 @@ export class Store {
392
433
  /** Espaces dont l'utilisateur est membre : son espace personnel d'abord, puis les espaces communs par nom. */
393
434
  listSpacesOf(userId) {
394
435
  const rows = this.db
395
- .prepare(`SELECT s.* FROM spaces s JOIN memberships m ON m.space_id = s.id
436
+ .prepare(`SELECT s.* FROM spaces s JOIN memberships m ON m.space_id = s.id
396
437
  WHERE m.user_id = ? ORDER BY s.kind = 'shared', s.name COLLATE NOCASE`)
397
438
  .all(userId);
398
439
  return rows.map(spaceFromRow);
399
440
  }
441
+ /** Tous les espaces : les espaces personnels d'abord, puis les espaces communs par nom. */
442
+ listAllSpaces() {
443
+ const rows = this.db.prepare("SELECT * FROM spaces ORDER BY kind = 'shared', name COLLATE NOCASE, id").all();
444
+ return rows.map(spaceFromRow);
445
+ }
400
446
  listMemberships(spaceId) {
401
447
  const rows = this.db.prepare("SELECT * FROM memberships WHERE space_id = ? ORDER BY joined_at, user_id").all(spaceId);
402
448
  return rows.map(membershipFromRow);
@@ -542,7 +588,7 @@ export class Store {
542
588
  /** Résultats saisis sur les points des tâches d'un espace. */
543
589
  listTestResultsInSpace(spaceId) {
544
590
  const rows = this.db
545
- .prepare(`SELECT c.task_id, r.checkpoint_id, r.tester_id, r.result FROM test_results r
591
+ .prepare(`SELECT c.task_id, r.checkpoint_id, r.tester_id, r.result FROM test_results r
546
592
  JOIN test_checkpoints c ON c.id = r.checkpoint_id JOIN tasks t ON t.id = c.task_id WHERE t.space_id = ?`)
547
593
  .all(spaceId);
548
594
  return rows.map((row) => ({
@@ -587,7 +633,7 @@ export class Store {
587
633
  /** Pièces jointes d'une tâche et de toutes ses sous-tâches. */
588
634
  listAttachmentsInSubtree(taskId) {
589
635
  const rows = this.db
590
- .prepare(`WITH RECURSIVE subtree(id) AS (SELECT ? UNION ALL SELECT t.id FROM tasks t JOIN subtree s ON t.parent_id = s.id)
636
+ .prepare(`WITH RECURSIVE subtree(id) AS (SELECT ? UNION ALL SELECT t.id FROM tasks t JOIN subtree s ON t.parent_id = s.id)
591
637
  SELECT a.* FROM attachments a JOIN subtree s ON a.task_id = s.id`)
592
638
  .all(taskId);
593
639
  return rows.map(attachmentFromRow);
@@ -617,7 +663,7 @@ export class Store {
617
663
  /** Demandes de promotion (`idea_promotion`) en attente ou en cours sur les idées d'un espace. */
618
664
  listOpenPromotionRequests(spaceId) {
619
665
  const rows = this.db
620
- .prepare(`SELECT r.* FROM agent_requests r JOIN ideas i ON i.id = r.context_idea_id
666
+ .prepare(`SELECT r.* FROM agent_requests r JOIN ideas i ON i.id = r.context_idea_id
621
667
  WHERE i.space_id = ? AND r.kind = 'idea_promotion' AND r.status IN ('pending', 'processing') ORDER BY r.created_at, r.id`)
622
668
  .all(spaceId);
623
669
  return rows.map(requestFromRow);
@@ -650,6 +696,21 @@ export class Store {
650
696
  .all({ spaceId, status, limit });
651
697
  return rows.map(requestFromRow);
652
698
  }
699
+ /**
700
+ * Échanges clos (traités ou en échec) d'un auteur dans un espace, antérieurs à
701
+ * la demande `before`, du plus ancien au plus récent : les `limit` derniers.
702
+ */
703
+ listClosedRequestsOfAuthorBefore(spaceId, authorId, before, limit) {
704
+ if (limit === 0)
705
+ return [];
706
+ const rows = this.db
707
+ .prepare(`SELECT * FROM agent_requests
708
+ WHERE target_space_id = @spaceId AND author_id = @authorId AND status IN ('done', 'failed')
709
+ AND (created_at < @createdAt OR (created_at = @createdAt AND id < @id))
710
+ ORDER BY created_at DESC, id DESC LIMIT @limit`)
711
+ .all({ spaceId, authorId, createdAt: before.createdAt, id: before.id, limit });
712
+ return rows.map(requestFromRow).reverse();
713
+ }
653
714
  /** Demandes d'un espace, de la plus récente à la plus ancienne, au plus `limit`. */
654
715
  listAgentRequestsOfSpace(spaceId, limit) {
655
716
  const rows = this.db
@@ -712,9 +773,9 @@ export class Store {
712
773
  */
713
774
  listNotifications(userId, limit, offset) {
714
775
  const rows = this.db
715
- .prepare(`SELECT n.* FROM notifications n JOIN tasks t ON t.id = n.task_id
716
- JOIN memberships m ON m.space_id = t.space_id AND m.user_id = n.user_id
717
- WHERE n.user_id = ? AND n.in_app = 1
776
+ .prepare(`SELECT n.* FROM notifications n JOIN tasks t ON t.id = n.task_id
777
+ JOIN memberships m ON m.space_id = t.space_id AND m.user_id = n.user_id
778
+ WHERE n.user_id = ? AND n.in_app = 1
718
779
  ORDER BY n.read_at IS NOT NULL, n.created_at DESC, n.id DESC LIMIT ? OFFSET ?`)
719
780
  .all(userId, limit, offset);
720
781
  return rows.map(notificationFromRow);
@@ -722,8 +783,8 @@ export class Store {
722
783
  /** Nombre de notifications non lues affichées dans l'application d'un utilisateur. */
723
784
  countUnreadNotifications(userId) {
724
785
  const row = this.db
725
- .prepare(`SELECT COUNT(*) AS n FROM notifications n JOIN tasks t ON t.id = n.task_id
726
- JOIN memberships m ON m.space_id = t.space_id AND m.user_id = n.user_id
786
+ .prepare(`SELECT COUNT(*) AS n FROM notifications n JOIN tasks t ON t.id = n.task_id
787
+ JOIN memberships m ON m.space_id = t.space_id AND m.user_id = n.user_id
727
788
  WHERE n.user_id = ? AND n.in_app = 1 AND n.read_at IS NULL`)
728
789
  .get(userId);
729
790
  return row.n;
@@ -752,8 +813,8 @@ export class Store {
752
813
  */
753
814
  listChangesForUser(userId, since, limit) {
754
815
  const rows = this.db
755
- .prepare(`SELECT * FROM changes WHERE seq > ?
756
- AND (user_id = ? OR space_id IN (SELECT space_id FROM memberships WHERE user_id = ?))
816
+ .prepare(`SELECT * FROM changes WHERE seq > ?
817
+ AND (user_id = ? OR space_id IN (SELECT space_id FROM memberships WHERE user_id = ?))
757
818
  ORDER BY seq LIMIT ?`)
758
819
  .all(since, userId, userId, limit);
759
820
  return rows.map(changeFromRow);
@@ -780,6 +841,11 @@ export class Store {
780
841
  this.pendingRequestListeners.add(listener);
781
842
  return () => this.pendingRequestListeners.delete(listener);
782
843
  }
844
+ /** Abonne `listener` aux espaces où le runner vient d'être autorisé, après la validation de la mutation. */
845
+ onRunnerAllowed(listener) {
846
+ this.runnerAllowedListeners.add(listener);
847
+ return () => this.runnerAllowedListeners.delete(listener);
848
+ }
783
849
  /**
784
850
  * Point unique d'écriture. `fn` reçoit une `Mutation` et s'exécute dans une
785
851
  * transaction exclusive ; à sa sortie, les invariants sont vérifiés sur l'état
@@ -791,6 +857,7 @@ export class Store {
791
857
  throw new Error("Store.mutate is not reentrant: use the Mutation passed to the running mutation.");
792
858
  let emails = [];
793
859
  let pending = [];
860
+ let allowed = [];
794
861
  const run = this.db.transaction(() => {
795
862
  const tx = new Mutation(this, actor);
796
863
  this.activeMutation = tx;
@@ -799,6 +866,7 @@ export class Store {
799
866
  tx.finish();
800
867
  emails = tx.emailNotificationIds;
801
868
  pending = tx.pendingRequestIds;
869
+ allowed = tx.runnerAllowedSpaceIds;
802
870
  return result;
803
871
  }
804
872
  finally {
@@ -815,6 +883,11 @@ export class Store {
815
883
  listener(request);
816
884
  }
817
885
  }
886
+ for (const id of allowed) {
887
+ const space = this.getSpace(id);
888
+ for (const listener of [...this.runnerAllowedListeners])
889
+ listener(space);
890
+ }
818
891
  return result;
819
892
  }
820
893
  }
@@ -833,6 +906,8 @@ export class Mutation {
833
906
  emailNotificationIds = [];
834
907
  /** Demandes passées en attente par la mutation : créées, ou libérées. */
835
908
  pendingRequestIds = [];
909
+ /** Espaces où la mutation a autorisé le runner. */
910
+ runnerAllowedSpaceIds = [];
836
911
  /** Demande pour laquelle agit l'agent, lue au début de la mutation. */
837
912
  request;
838
913
  /** Jeton avec lequel agit l'agent. */
@@ -917,6 +992,7 @@ export class Mutation {
917
992
  displayName: input.displayName ?? null,
918
993
  passwordHash: input.passwordHash ?? null,
919
994
  status: input.status,
995
+ isAdmin: input.isAdmin ?? false,
920
996
  createdAt: this.now,
921
997
  }, "user");
922
998
  if (user.status !== "invited" && user.displayName === null) {
@@ -925,8 +1001,8 @@ export class Mutation {
925
1001
  if (this.store.findUserByEmail(user.email))
926
1002
  throw new InvariantError(`An account already exists for ${user.email}.`);
927
1003
  this.db
928
- .prepare("INSERT INTO users (id, email, display_name, password_hash, status, created_at) VALUES (?, ?, ?, ?, ?, ?)")
929
- .run(user.id, user.email, user.displayName, user.passwordHash, user.status, user.createdAt);
1004
+ .prepare("INSERT INTO users (id, email, display_name, password_hash, status, is_admin, created_at) VALUES (?, ?, ?, ?, ?, ?, ?)")
1005
+ .run(user.id, user.email, user.displayName, user.passwordHash, user.status, user.isAdmin ? 1 : 0, user.createdAt);
930
1006
  this.db.prepare("INSERT INTO user_settings (user_id) VALUES (?)").run(user.id);
931
1007
  this.touch("user", user.id, null, user.id);
932
1008
  return user;
@@ -943,6 +1019,13 @@ export class Mutation {
943
1019
  this.touch("user", id, null, id);
944
1020
  return user;
945
1021
  }
1022
+ /** Accorde ou retire le rôle d'administrateur du site. */
1023
+ setUserAdmin(id, isAdmin) {
1024
+ const user = { ...this.store.getUser(id), isAdmin };
1025
+ this.db.prepare("UPDATE users SET is_admin = ? WHERE id = ?").run(isAdmin ? 1 : 0, id);
1026
+ this.touch("user", id, null, id);
1027
+ return user;
1028
+ }
946
1029
  // --- Sessions et liens d'accès ---
947
1030
  createSession(session) {
948
1031
  const valid = validate(SessionSchema, session, "session");
@@ -1009,13 +1092,13 @@ export class Mutation {
1009
1092
  createAgentToken(input) {
1010
1093
  if (this.actor.type !== "user")
1011
1094
  throw new InvariantError("Only a user can create an agent token.");
1012
- const token = validate(AgentTokenSchema, { id: newId(), userId: this.actor.userId, ...input, createdAt: this.now, lastUsedAt: null, revokedAt: null }, "agent token");
1095
+ const token = validate(AgentTokenSchema, { id: newId(), userId: this.actor.userId, ...input, isRunner: input.isRunner ?? false, createdAt: this.now, lastUsedAt: null, revokedAt: null }, "agent token");
1013
1096
  if (!this.store.isMember(token.spaceId, token.userId))
1014
1097
  throw new ForbiddenError("An agent token can only cover a space its owner is a member of.");
1015
1098
  this.db
1016
- .prepare(`INSERT INTO agent_tokens (id, user_id, space_id, name, prefix, token_hash, created_at, expires_at)
1017
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
1018
- .run(token.id, token.userId, token.spaceId, token.name, token.prefix, token.tokenHash, token.createdAt, token.expiresAt);
1099
+ .prepare(`INSERT INTO agent_tokens (id, user_id, space_id, name, prefix, token_hash, created_at, expires_at, is_runner)
1100
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1101
+ .run(token.id, token.userId, token.spaceId, token.name, token.prefix, token.tokenHash, token.createdAt, token.expiresAt, token.isRunner ? 1 : 0);
1019
1102
  this.touch("agent_token", token.id, null, token.userId);
1020
1103
  return token;
1021
1104
  }
@@ -1030,6 +1113,14 @@ export class Mutation {
1030
1113
  this.touch("agent_token", id, null, token.userId);
1031
1114
  return { ...token, revokedAt: this.now };
1032
1115
  }
1116
+ /** Remplace la valeur d'un jeton du runner : l'ancienne valeur est aussitôt refusée. */
1117
+ rotateRunnerToken(id, prefix, tokenHash) {
1118
+ const token = this.store.findAgentToken(id);
1119
+ if (!token || !token.isRunner)
1120
+ throw new NotFoundError(`Runner token ${id} not found.`);
1121
+ this.db.prepare("UPDATE agent_tokens SET prefix = ?, token_hash = ? WHERE id = ?").run(prefix, tokenHash, id);
1122
+ return { ...token, prefix, tokenHash };
1123
+ }
1033
1124
  /** Date du dernier usage d'un jeton ; le journal des changements n'en garde pas trace. */
1034
1125
  recordAgentTokenUse(id, at) {
1035
1126
  this.db.prepare("UPDATE agent_tokens SET last_used_at = ? WHERE id = ?").run(at, id);
@@ -1105,7 +1196,7 @@ export class Mutation {
1105
1196
  // --- Espaces et membres ---
1106
1197
  /** Crée un espace et en fait de son propriétaire le membre `owner`. */
1107
1198
  createSpace(input) {
1108
- const space = validate(SpaceSchema, { id: newId(), name: input.name, kind: input.kind, ownerId: input.ownerId, keyPrefix: input.keyPrefix, nextTaskNumber: 1, createdAt: this.now }, "space");
1199
+ const space = validate(SpaceSchema, { id: newId(), name: input.name, kind: input.kind, ownerId: input.ownerId, keyPrefix: input.keyPrefix, nextTaskNumber: 1, runnerAllowed: false, createdAt: this.now }, "space");
1109
1200
  this.store.getUser(space.ownerId);
1110
1201
  if (space.kind === "personal" && this.store.findPersonalSpace(space.ownerId)) {
1111
1202
  throw new InvariantError(`User ${space.ownerId} already has a personal space.`);
@@ -1117,6 +1208,15 @@ export class Mutation {
1117
1208
  this.insertMembership(space.id, space.ownerId, "owner");
1118
1209
  return space;
1119
1210
  }
1211
+ /** Autorise ou interdit le runner sur un espace. */
1212
+ setSpaceRunnerAllowed(id, allowed) {
1213
+ const space = { ...this.store.getSpace(id), runnerAllowed: allowed };
1214
+ this.db.prepare("UPDATE spaces SET runner_allowed = ? WHERE id = ?").run(allowed ? 1 : 0, id);
1215
+ if (allowed)
1216
+ this.runnerAllowedSpaceIds.push(id);
1217
+ this.touch("space", id, id, null);
1218
+ return space;
1219
+ }
1120
1220
  renameSpace(id, name) {
1121
1221
  const space = validate(SpaceSchema, { ...this.store.getSpace(id), name }, "space");
1122
1222
  this.db.prepare("UPDATE spaces SET name = ? WHERE id = ?").run(space.name, id);
@@ -1132,7 +1232,7 @@ export class Mutation {
1132
1232
  this.touch("membership", `${id}:${membership.userId}`, id, membership.userId);
1133
1233
  }
1134
1234
  this.db
1135
- .prepare(`DELETE FROM entity_versions WHERE (entity_type = 'task' AND entity_id IN (SELECT id FROM tasks WHERE space_id = ?))
1235
+ .prepare(`DELETE FROM entity_versions WHERE (entity_type = 'task' AND entity_id IN (SELECT id FROM tasks WHERE space_id = ?))
1136
1236
  OR (entity_type = 'idea' AND entity_id IN (SELECT id FROM ideas WHERE space_id = ?))`)
1137
1237
  .run(id, id);
1138
1238
  this.db.prepare("DELETE FROM spaces WHERE id = ?").run(id);
@@ -1158,6 +1258,7 @@ export class Mutation {
1158
1258
  if (!this.store.findMembership(spaceId, userId))
1159
1259
  throw new NotFoundError(`User ${userId} is not a member of space ${spaceId}.`);
1160
1260
  this.db.prepare("DELETE FROM memberships WHERE space_id = ? AND user_id = ?").run(spaceId, userId);
1261
+ this.db.prepare("DELETE FROM space_notification_settings WHERE space_id = ? AND user_id = ?").run(spaceId, userId);
1161
1262
  this.touch("membership", `${spaceId}:${userId}`, spaceId, userId);
1162
1263
  const tokenIds = this.db.prepare("SELECT id FROM agent_tokens WHERE space_id = ? AND user_id = ? AND revoked_at IS NULL").pluck().all(spaceId, userId);
1163
1264
  this.db.prepare("UPDATE agent_tokens SET revoked_at = ? WHERE space_id = ? AND user_id = ? AND revoked_at IS NULL").run(this.now, spaceId, userId);
@@ -1195,8 +1296,8 @@ export class Mutation {
1195
1296
  version: 1,
1196
1297
  }, "task");
1197
1298
  this.db
1198
- .prepare(`INSERT INTO tasks (id, space_id, number, parent_id, depth, type, title, description, status, priority, position,
1199
- created_by, created_at, updated_at, version)
1299
+ .prepare(`INSERT INTO tasks (id, space_id, number, parent_id, depth, type, title, description, status, priority, position,
1300
+ created_by, created_at, updated_at, version)
1200
1301
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1201
1302
  .run(task.id, task.spaceId, task.number, task.parentId, task.depth, task.type, task.title, task.description, task.status, task.priority, task.position, task.createdBy, task.createdAt, task.updatedAt, task.version);
1202
1303
  this.db.prepare("UPDATE spaces SET next_task_number = next_task_number + 1 WHERE id = ?").run(space.id);
@@ -1230,7 +1331,7 @@ export class Mutation {
1230
1331
  const testedTaskId = fields.testedTaskId !== undefined ? fields.testedTaskId : type === "standard" ? null : current.testedTaskId;
1231
1332
  const task = validate(TaskSchema, { ...current, ...fields, parentId, depth, status, position, testedTaskId, updatedAt: this.now }, "task");
1232
1333
  this.db
1233
- .prepare(`UPDATE tasks SET parent_id = ?, depth = ?, type = ?, title = ?, description = ?, status = ?, priority = ?,
1334
+ .prepare(`UPDATE tasks SET parent_id = ?, depth = ?, type = ?, title = ?, description = ?, status = ?, priority = ?,
1234
1335
  position = ?, tested_task_id = ?, updated_at = ? WHERE id = ?`)
1235
1336
  .run(task.parentId, task.depth, task.type, task.title, task.description, task.status, task.priority, task.position, task.testedTaskId, task.updatedAt, id);
1236
1337
  this.touch("task", id, task.spaceId, null);
@@ -1489,7 +1590,7 @@ export class Mutation {
1489
1590
  updatedAt: this.now,
1490
1591
  }, "test result");
1491
1592
  this.db
1492
- .prepare(`INSERT INTO test_results (checkpoint_id, tester_id, result, comment, updated_at) VALUES (?, ?, ?, ?, ?)
1593
+ .prepare(`INSERT INTO test_results (checkpoint_id, tester_id, result, comment, updated_at) VALUES (?, ?, ?, ?, ?)
1493
1594
  ON CONFLICT (checkpoint_id, tester_id) DO UPDATE SET result = excluded.result, comment = excluded.comment, updated_at = excluded.updated_at`)
1494
1595
  .run(result.checkpointId, result.testerId, result.result, result.comment, result.updatedAt);
1495
1596
  this.touch("test_result", `${checkpointId}:${testerId}`, task.spaceId, null, task.id);
@@ -1652,7 +1753,7 @@ export class Mutation {
1652
1753
  createdAt: this.now,
1653
1754
  }, "idea");
1654
1755
  this.db
1655
- .prepare(`INSERT INTO ideas (id, space_id, author_id, text, origin, request_id, promoted_task_id, archived_at, created_at)
1756
+ .prepare(`INSERT INTO ideas (id, space_id, author_id, text, origin, request_id, promoted_task_id, archived_at, created_at)
1656
1757
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1657
1758
  .run(idea.id, idea.spaceId, idea.authorId, idea.text, idea.origin, idea.requestId, idea.promotedTaskId, idea.archivedAt, idea.createdAt);
1658
1759
  this.touch("idea", idea.id, idea.spaceId, null);
@@ -1709,8 +1810,8 @@ export class Mutation {
1709
1810
  }, "request");
1710
1811
  this.store.getSpace(request.targetSpaceId);
1711
1812
  this.db
1712
- .prepare(`INSERT INTO agent_requests (id, author_id, target_space_id, kind, context_task_id, context_idea_id,
1713
- text, status, created_at, updated_at)
1813
+ .prepare(`INSERT INTO agent_requests (id, author_id, target_space_id, kind, context_task_id, context_idea_id,
1814
+ text, status, created_at, updated_at)
1714
1815
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1715
1816
  .run(request.id, request.authorId, request.targetSpaceId, request.kind, request.contextTaskId, request.contextIdeaId, request.text, request.status, request.createdAt, request.updatedAt);
1716
1817
  this.createdRequestIds.add(request.id);
@@ -1835,7 +1936,7 @@ export class Mutation {
1835
1936
  tokenId: token.id,
1836
1937
  }, "agent action");
1837
1938
  this.db
1838
- .prepare(`INSERT INTO agent_actions (id, request_id, seq, entity_type, entity_id, operation, before, after, summary, created_at, token_id)
1939
+ .prepare(`INSERT INTO agent_actions (id, request_id, seq, entity_type, entity_id, operation, before, after, summary, created_at, token_id)
1839
1940
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1840
1941
  .run(action.id, action.requestId, action.seq, action.entityType, action.entityId, action.operation, action.before === null ? null : JSON.stringify(action.before), action.after === null ? null : JSON.stringify(action.after), action.summary, action.createdAt, action.tokenId);
1841
1942
  this.touch("agent_action", action.id, request.targetSpaceId, null, input.taskId);
@@ -1858,13 +1959,14 @@ export class Mutation {
1858
1959
  return this.actor.type === "user" && this.actor.userId === userId;
1859
1960
  }
1860
1961
  /**
1861
- * Notifie un utilisateur selon ses préférences : la notification apparaît
1962
+ * Notifie un utilisateur selon ses préférences dans l'espace de la tâche
1963
+ * (celles de l'espace, sinon celles du compte) : la notification apparaît
1862
1964
  * dans l'application si elle y est activée, et son identifiant est retenu
1863
1965
  * pour l'envoi de l'e-mail si celui-ci est activé. Rien n'est créé quand les
1864
1966
  * deux sont désactivés.
1865
1967
  */
1866
1968
  notify(type, userId, taskId, commentId) {
1867
- const settings = this.store.getUserSettings(userId);
1969
+ const settings = this.store.notificationPreferences(userId, this.store.getTask(taskId).spaceId);
1868
1970
  const inApp = type === "assigned" ? settings.notifyAssignApp : settings.notifyCommentApp;
1869
1971
  const email = type === "assigned" ? settings.notifyAssignEmail : settings.notifyCommentEmail;
1870
1972
  if (!inApp && !email)
@@ -1885,7 +1987,7 @@ export class Mutation {
1885
1987
  emailError: null,
1886
1988
  }, "notification");
1887
1989
  this.db
1888
- .prepare(`INSERT INTO notifications (id, user_id, type, task_id, actor_type, actor_id, request_id, comment_id, in_app, created_at)
1990
+ .prepare(`INSERT INTO notifications (id, user_id, type, task_id, actor_type, actor_id, request_id, comment_id, in_app, created_at)
1889
1991
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
1890
1992
  .run(notification.id, notification.userId, notification.type, notification.taskId, notification.actorType, notification.actorId, notification.requestId, notification.commentId, inApp ? 1 : 0, notification.createdAt);
1891
1993
  if (inApp)
@@ -1927,12 +2029,33 @@ export class Mutation {
1927
2029
  updateUserSettings(userId, patch) {
1928
2030
  const settings = validate(UserSettingsSchema, { ...this.store.getUserSettings(userId), ...definedFields(patch) }, "settings");
1929
2031
  this.db
1930
- .prepare(`UPDATE user_settings SET notify_assign_app = ?, notify_assign_email = ?, notify_comment_app = ?, notify_comment_email = ?
1931
- WHERE user_id = ?`)
1932
- .run(settings.notifyAssignApp ? 1 : 0, settings.notifyAssignEmail ? 1 : 0, settings.notifyCommentApp ? 1 : 0, settings.notifyCommentEmail ? 1 : 0, userId);
2032
+ .prepare(`UPDATE user_settings SET notify_assign_app = ?, notify_assign_email = ?, notify_comment_app = ?, notify_comment_email = ?,
2033
+ assistant_history = ? WHERE user_id = ?`)
2034
+ .run(settings.notifyAssignApp ? 1 : 0, settings.notifyAssignEmail ? 1 : 0, settings.notifyCommentApp ? 1 : 0, settings.notifyCommentEmail ? 1 : 0, settings.assistantHistory, userId);
1933
2035
  this.touch("user_settings", userId, null, userId);
1934
2036
  return settings;
1935
2037
  }
2038
+ /**
2039
+ * Fixe les préférences de notification d'un membre propres à un espace ;
2040
+ * null les retire, et il y suit de nouveau celles de son compte.
2041
+ */
2042
+ setSpaceNotificationSettings(userId, spaceId, preferences) {
2043
+ if (!this.store.findMembership(spaceId, userId))
2044
+ throw new NotFoundError(`User ${userId} is not a member of space ${spaceId}.`);
2045
+ this.touch("user_settings", userId, null, userId);
2046
+ if (preferences === null) {
2047
+ this.db.prepare("DELETE FROM space_notification_settings WHERE user_id = ? AND space_id = ?").run(userId, spaceId);
2048
+ return null;
2049
+ }
2050
+ const settings = validate(SpaceNotificationSettingsSchema, { userId, spaceId, ...preferences }, "space notification settings");
2051
+ this.db
2052
+ .prepare(`INSERT INTO space_notification_settings (user_id, space_id, notify_assign_app, notify_assign_email, notify_comment_app, notify_comment_email)
2053
+ VALUES (?, ?, ?, ?, ?, ?)
2054
+ ON CONFLICT (user_id, space_id) DO UPDATE SET notify_assign_app = excluded.notify_assign_app, notify_assign_email = excluded.notify_assign_email,
2055
+ notify_comment_app = excluded.notify_comment_app, notify_comment_email = excluded.notify_comment_email`)
2056
+ .run(userId, spaceId, settings.notifyAssignApp ? 1 : 0, settings.notifyAssignEmail ? 1 : 0, settings.notifyCommentApp ? 1 : 0, settings.notifyCommentEmail ? 1 : 0);
2057
+ return settings;
2058
+ }
1936
2059
  // --- Fin de mutation ---
1937
2060
  /** Vérifie les invariants sur l'état final, puis écrit les versions et le journal. */
1938
2061
  finish() {
@@ -2009,8 +2132,8 @@ export class Mutation {
2009
2132
  }
2010
2133
  }
2011
2134
  const outsiders = this.db
2012
- .prepare(`SELECT t.number AS number, x.user_id AS user_id FROM task_assignees x JOIN tasks t ON t.id = x.task_id
2013
- WHERE t.space_id = ? AND NOT EXISTS (SELECT 1 FROM memberships m WHERE m.space_id = t.space_id AND m.user_id = x.user_id)
2135
+ .prepare(`SELECT t.number AS number, x.user_id AS user_id FROM task_assignees x JOIN tasks t ON t.id = x.task_id
2136
+ WHERE t.space_id = ? AND NOT EXISTS (SELECT 1 FROM memberships m WHERE m.space_id = t.space_id AND m.user_id = x.user_id)
2014
2137
  ORDER BY t.number LIMIT 1`)
2015
2138
  .get(space.id);
2016
2139
  if (outsiders) {
@@ -2082,7 +2205,7 @@ export class Mutation {
2082
2205
  const actorId = this.actingUserId;
2083
2206
  const requestId = this.request?.id ?? null;
2084
2207
  const tokenId = this.token?.id ?? null;
2085
- const insert = this.db.prepare(`INSERT INTO entity_versions (entity_type, entity_id, version, snapshot, actor_type, actor_id, request_id, created_at, token_id)
2208
+ const insert = this.db.prepare(`INSERT INTO entity_versions (entity_type, entity_id, version, snapshot, actor_type, actor_id, request_id, created_at, token_id)
2086
2209
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
2087
2210
  for (const entry of this.touched.values()) {
2088
2211
  if (!VERSIONED.has(entry.entityType))
@@ -2104,7 +2227,7 @@ export class Mutation {
2104
2227
  }
2105
2228
  /** Une ligne du journal par entité modifiée, avec l'auteur de la mutation. */
2106
2229
  writeChanges() {
2107
- const insert = this.db.prepare(`INSERT INTO changes (space_id, user_id, entity_type, entity_id, task_id, at, actor_type, actor_id, request_id)
2230
+ const insert = this.db.prepare(`INSERT INTO changes (space_id, user_id, entity_type, entity_id, task_id, at, actor_type, actor_id, request_id)
2108
2231
  VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`);
2109
2232
  const actorId = this.actingUserId;
2110
2233
  const requestId = this.request?.id ?? null;
@@ -1,4 +1,5 @@
1
1
  import { type ApiRequestData, type ApiResponseData, type ApiErrorKind } from "../shared/schemas/gui-api.schema.js";
2
+ import { type RunnerStatus } from "../shared/schemas/admin-api.schema.js";
2
3
  import type { AttachmentStorage } from "./core/attachments.js";
3
4
  import type { Mailer } from "./core/mail.js";
4
5
  import type { Store } from "./core/store.js";
@@ -26,6 +27,11 @@ export interface GuiApiContext {
26
27
  attachments: AttachmentStorage;
27
28
  /** Dictée vocale (API Groq). */
28
29
  transcriber: Transcriber;
30
+ /** Runner du site : son état, et le lancement d'une session ponctuelle (null quand il est désactivé). */
31
+ runner: {
32
+ status: RunnerStatus;
33
+ runNow: ((spaceId: string) => void) | null;
34
+ };
29
35
  /** Horloge, imposée par les tests. */
30
36
  now?: () => Date;
31
37
  }
@@ -6,8 +6,8 @@ import { acceptReceivedInvitation, createSharedSpace, declineReceivedInvitation,
6
6
  import { addChecklistItem, addComment, createTask, deleteAttachment, deleteChecklistItem, deleteComment, deleteTask, downloadAttachment, getBoard, getTaskDetail, listChanges, moveChecklistItem, moveTask, setAssignees, setTags, updateChecklistItem, updateComment, updateTask, uploadAttachment, } from "./core/tasks.js";
7
7
  import { addCheckpoint, deleteCheckpoint, moveCheckpoint, saveReviewDraft, setTestResult, submitReview, updateCheckpoint } from "./core/test-tasks.js";
8
8
  import { AddChecklistItemRequestSchema, CheckpointTextRequestSchema, CommentRequestSchema, CreateTaskRequestSchema, MoveAfterRequestSchema, MoveTaskRequestSchema, ReviewDraftRequestSchema, SetAssigneesRequestSchema, SetTagsRequestSchema, SetTestResultRequestSchema, SubmitReviewRequestSchema, TaskFiltersSchema, UpdateChecklistItemRequestSchema, UpdateTaskRequestSchema, UploadAttachmentRequestSchema, } from "../shared/schemas/tasks-api.schema.js";
9
- import { getSettings, listNotifications, markAllNotificationsRead, markNotificationRead, unreadCount, updateSettings } from "./core/notifications.js";
10
- import { NotificationListQuerySchema, UpdateSettingsRequestSchema } from "../shared/schemas/notifications-api.schema.js";
9
+ import { getSettings, getSpaceNotificationSettings, listNotifications, markAllNotificationsRead, markNotificationRead, unreadCount, updateSettings, updateSpaceNotificationSettings } from "./core/notifications.js";
10
+ import { NotificationListQuerySchema, UpdateSettingsRequestSchema, UpdateSpaceNotificationSettingsRequestSchema } from "../shared/schemas/notifications-api.schema.js";
11
11
  import { cancelRequest, createRequest, getRequest, listRequests, releaseRequest, undoAction, undoRequest, updateRequest } from "./core/requests.js";
12
12
  import { listTaskVersions, restoreTaskVersion } from "./core/task-history.js";
13
13
  import { CreateRequestSchema, UpdateRequestSchema } from "../shared/schemas/agent-api.schema.js";
@@ -15,6 +15,8 @@ import { createIdea, delegatePromotion, listIdeas, promoteIdea, setIdeaArchived
15
15
  import { CreateIdeaRequestSchema, PromoteIdeaRequestSchema, UpdateIdeaRequestSchema } from "../shared/schemas/ideas-api.schema.js";
16
16
  import { createAgentToken, listAgentTokens, revokeAgentToken } from "./core/agent-tokens.js";
17
17
  import { CreateTokenRequestSchema } from "../shared/schemas/tokens-api.schema.js";
18
+ import { listAdminSpaces, processQueueNow, requireAdmin, setRunnerAllowed } from "./core/admin.js";
19
+ import { SetRunnerAllowedRequestSchema } from "../shared/schemas/admin-api.schema.js";
18
20
  import { TranscriptionError } from "./core/transcription.js";
19
21
  import { MAX_TRANSCRIPTION_BYTES, TranscribeRequestSchema } from "../shared/schemas/transcription-api.schema.js";
20
22
  /** Erreur d'une requête de la GUI, avec la nature qui fixe son code HTTP. */
@@ -89,7 +91,7 @@ function parseBody(schema, body) {
89
91
  return parsed.data;
90
92
  }
91
93
  function publicUser(user) {
92
- return { id: user.id, email: user.email, displayName: user.displayName, status: user.status };
94
+ return { id: user.id, email: user.email, displayName: user.displayName, status: user.status, isAdmin: user.isAdmin };
93
95
  }
94
96
  const ok = { ok: true };
95
97
  /** Taille lisible d'un nombre d'octets, en Mo. */
@@ -287,7 +289,7 @@ route("POST", "/ideas/:ideaId/promote", (ctx, { user, request, params }) => ({
287
289
  }));
288
290
  route("POST", "/ideas/:ideaId/delegate", (ctx, { user, params }) => ({ body: delegatePromotion(ctx.store, user.id, params.ideaId) }));
289
291
  // --- File de l'agent et historique -------------------------------------------------
290
- route("GET", "/spaces/:spaceId/requests", (ctx, { user, params }) => ({ body: listRequests(ctx.store, user.id, params.spaceId, now(ctx)) }));
292
+ route("GET", "/spaces/:spaceId/requests", (ctx, { user, params }) => ({ body: listRequests(ctx.store, user.id, params.spaceId, ctx.runner.status.disabledReason, now(ctx)) }));
291
293
  route("POST", "/spaces/:spaceId/requests", (ctx, { user, request, params }) => ({
292
294
  body: createRequest(ctx.store, user.id, params.spaceId, parseBody(CreateRequestSchema, request.body)),
293
295
  }));
@@ -322,6 +324,12 @@ route("GET", "/settings", (ctx, { user }) => ({ body: getSettings(ctx.store, ctx
322
324
  route("PUT", "/settings", (ctx, { user, request }) => ({
323
325
  body: updateSettings(ctx.store, ctx, user.id, parseBody(UpdateSettingsRequestSchema, request.body)),
324
326
  }));
327
+ route("GET", "/spaces/:spaceId/notification-settings", (ctx, { user, params }) => ({
328
+ body: getSpaceNotificationSettings(ctx.store, ctx, user.id, params.spaceId),
329
+ }));
330
+ route("PUT", "/spaces/:spaceId/notification-settings", (ctx, { user, request, params }) => ({
331
+ body: updateSpaceNotificationSettings(ctx.store, ctx, user.id, params.spaceId, parseBody(UpdateSpaceNotificationSettingsRequestSchema, request.body)),
332
+ }));
325
333
  // --- Jetons d'agent ---------------------------------------------------------------
326
334
  route("GET", "/tokens", (ctx, { user }) => ({ body: listAgentTokens(ctx.store, user.id, now(ctx)) }));
327
335
  route("POST", "/tokens", (ctx, { user, request }) => {
@@ -331,6 +339,19 @@ route("POST", "/tokens", (ctx, { user, request }) => {
331
339
  return { body: createAgentToken(ctx.store, user.id, input, ctx.publicUrl, now(ctx)) };
332
340
  });
333
341
  route("POST", "/tokens/:tokenId/revoke", (ctx, { user, params }) => ({ body: revokeAgentToken(ctx.store, user.id, params.tokenId, now(ctx)) }));
342
+ // --- Administration ----------------------------------------------------------------
343
+ route("GET", "/admin/spaces", (ctx, { user }) => ({ body: listAdminSpaces(ctx.store, user.id, ctx.runner.status) }));
344
+ route("POST", "/spaces/:spaceId/runner/run", (ctx, { user, params }) => {
345
+ requireAdmin(ctx.store, user.id);
346
+ const runNow = ctx.runner.runNow;
347
+ if (!runNow)
348
+ throw new ApiError("unavailable", `Runner désactivé : ${ctx.runner.status.disabledReason}`);
349
+ return { body: processQueueNow(ctx.store, user.id, params.spaceId, runNow) };
350
+ });
351
+ route("PUT", "/admin/spaces/:spaceId/runner", (ctx, { user, request, params }) => {
352
+ const input = parseBody(SetRunnerAllowedRequestSchema, request.body);
353
+ return { body: setRunnerAllowed(ctx.store, user.id, params.spaceId, input.allowed) };
354
+ });
334
355
  // --- Dictée ------------------------------------------------------------------------
335
356
  route("POST", "/transcribe", async (ctx, { request }) => {
336
357
  const input = parseBody(TranscribeRequestSchema, request.body);
package/dist/src/index.js CHANGED
@@ -12,6 +12,9 @@ Usage :
12
12
  mcp-organizer --server Site web : interface, API et base de données, sur HOST:PORT.
13
13
  mcp-organizer --set-user [email] [--name="Prénom Nom"] [--non-interactive]
14
14
  Crée un compte ou remplace son mot de passe.
15
+ mcp-organizer --set-admin <email> [--revoke]
16
+ Accorde (ou retire) le rôle d'administrateur du site à un compte actif.
17
+ Le premier compte créé est administrateur d'office.
15
18
  mcp-organizer --setup [options] Installe le site : .env, unité systemd, bloc nginx, premier compte
16
19
  (interactif, ou --non-interactive ; --setup --help pour les options).
17
20
  mcp-organizer --backup <fichier> [--force]
@@ -33,7 +36,7 @@ Le mode MCP exige ORGANIZER_URL (URL publique du site, en https hors de la machi
33
36
  et ORGANIZER_TOKEN (jeton d'agent), fournis par son environnement.
34
37
  Voir le README pour la liste complète des variables.`;
35
38
  /** Commandes qui ouvrent la base du site ; toute autre invocation démarre le mode MCP. */
36
- const SITE_COMMANDS = ["--server", "--set-user", "--setup", "--backup"];
39
+ const SITE_COMMANDS = ["--server", "--set-user", "--set-admin", "--setup", "--backup"];
37
40
  async function main() {
38
41
  const argv = process.argv.slice(2);
39
42
  if (argv.includes("--setup") && (argv.includes("--help") || argv.includes("-h"))) {
@@ -87,6 +90,10 @@ async function main() {
87
90
  const [{ runSetUser }, { resolveDataDir }] = await Promise.all([import("./cli/set-user.js"), import("./core/data-dir.js")]);
88
91
  process.exit(await runSetUser(args, resolveDataDir(rootDir)));
89
92
  }
93
+ if (args.includes("--set-admin")) {
94
+ const [{ runSetAdmin }, { resolveDataDir }] = await Promise.all([import("./cli/set-admin.js"), import("./core/data-dir.js")]);
95
+ process.exit(await runSetAdmin(args, resolveDataDir(rootDir)));
96
+ }
90
97
  if (args.includes("--backup")) {
91
98
  const [{ runBackup }, { resolveDataDir }] = await Promise.all([import("./cli/backup.js"), import("./core/data-dir.js")]);
92
99
  process.exit(await runBackup(args, resolveDataDir(rootDir)));