@imenam/mcp-organizer 1.0.1 → 1.0.3
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 +35 -7
- package/dist/shared/schemas/agent-api.schema.d.ts +1 -0
- package/dist/shared/schemas/agent-api.schema.js +5 -2
- package/dist/shared/schemas/agent-tools.schema.d.ts +99 -0
- package/dist/shared/schemas/agent-tools.schema.js +38 -0
- package/dist/src/agent-api/notifications.d.ts +16 -0
- package/dist/src/agent-api/notifications.js +92 -0
- package/dist/src/agent-api/router.js +2 -1
- package/dist/src/cli/watch.d.ts +1 -0
- package/dist/src/cli/watch.js +54 -0
- package/dist/src/core/requests.d.ts +2 -1
- package/dist/src/core/requests.js +3 -2
- package/dist/src/core/store.d.ts +12 -0
- package/dist/src/core/store.js +26 -0
- package/dist/src/core/watchers.d.ts +30 -0
- package/dist/src/core/watchers.js +50 -0
- package/dist/src/index.js +10 -0
- package/dist/src/mcp/client.d.ts +16 -1
- package/dist/src/mcp/client.js +29 -4
- package/dist/src/mcp/notifications.d.ts +30 -0
- package/dist/src/mcp/notifications.js +140 -0
- package/dist/src/mcp/start.d.ts +1 -1
- package/dist/src/mcp/start.js +52 -26
- package/dist/src/mcp/watch-handoff.d.ts +35 -0
- package/dist/src/mcp/watch-handoff.js +91 -0
- package/dist/src/server/app.js +23 -8
- package/gui/dist/assets/{index-lnvnkDdg.css → index-BVN0agCc.css} +1 -1
- package/gui/dist/assets/index-DI1_kr81.js +70 -0
- package/gui/dist/index.html +2 -2
- package/package.json +1 -1
- package/gui/dist/assets/index-DwkXx4Q2.js +0 -70
package/README.md
CHANGED
|
@@ -11,7 +11,7 @@ Un seul paquet, deux modes :
|
|
|
11
11
|
| `mcp-organizer --server` | **Site** : processus web permanent, derrière un reverse proxy (nginx). Il détient la base, sert l'interface, son API et l'API des agents (`/api/agent/*`). |
|
|
12
12
|
| `mcp-organizer` | **MCP** (stdio) : serveur MCP de l'agent, sans base ni logique métier. Configuré par `ORGANIZER_URL` et `ORGANIZER_TOKEN`, il relaie chaque appel d'outil à l'API des agents du site. Il peut tourner sur n'importe quelle machine, notamment dans une gateway de mcp-gateway-manager. |
|
|
13
13
|
|
|
14
|
-
Autres commandes : `--setup` (installation du site : `.env`, unité systemd, bloc nginx, premier compte), `--set-user` (compte), `--backup` (sauvegarde de la base et des pièces jointes), `--claude-setup-mcp` (entrée `.mcp.json` du mode MCP), `--version`, `--help`. Chaque mode et chaque commande chargent leur code à la demande.
|
|
14
|
+
Autres commandes : `--setup` (installation du site : `.env`, unité systemd, bloc nginx, premier compte), `--set-user` (compte), `--backup` (sauvegarde de la base et des pièces jointes), `--claude-setup-mcp` (entrée `.mcp.json` du mode MCP), `--watch` (veilleur de notifications de l'agent, lancé par son outil `Monitor`), `--version`, `--help`. Chaque mode et chaque commande chargent leur code à la demande.
|
|
15
15
|
|
|
16
16
|
Prérequis : Node.js 20 ou plus récent.
|
|
17
17
|
|
|
@@ -201,12 +201,14 @@ Les outils MCP sont destinés à l'agent ; leurs schémas JSON sont écrits à l
|
|
|
201
201
|
| `promote_idea` | `POST ideas/:id/promote` | Crée une tâche à partir d'une idée et marque l'idée promue |
|
|
202
202
|
| `complete_request` | `POST requests/:id/complete` | Termine la demande avec la réponse à l'auteur |
|
|
203
203
|
| `fail_request` | `POST requests/:id/fail` | Termine la demande en échec avec la raison |
|
|
204
|
+
| `enable_notifications` | `GET notifications` | Exécuté par le mode MCP lui-même : dépose le relais du veilleur et rend l'appel `Monitor` à faire, ou, derrière mcp-http-gateway, la spécification de veille du relais (voir « Notifications de l'agent ») |
|
|
205
|
+
| `wait_for_requests` | `GET requests/wait` | Outil d'attente du veilleur de la gateway : attend qu'une demande passe en attente (`cursor`, `timeout_seconds` de 1 à 50, `watcher`) et rend ses lignes et le curseur suivant dans `_meta["gateway/events"]` |
|
|
204
206
|
|
|
205
207
|
Chaque écriture enregistre une action dans le lot de la demande (avec le jeton), crée une version de l'entité (avec le jeton), alimente le journal des changements et les notifications. Une erreur dit à l'agent quoi faire ensuite (« call claim_request on it first », « Call search_tasks to find it »…).
|
|
206
208
|
|
|
207
209
|
### Boucle de travail de l'agent
|
|
208
210
|
|
|
209
|
-
|
|
211
|
+
L'agent traite la file quand l'utilisateur le lui demande, ou quand son veilleur lui annonce une demande (voir « Notifications de l'agent ») ; les `instructions` du serveur présentent les concepts (espace, tâche, demande, lot, portée), les notifications et la boucle :
|
|
210
212
|
|
|
211
213
|
1. `list_requests` ;
|
|
212
214
|
2. `claim_request` sur la plus ancienne (`get_context` montre l'espace à tout moment) ;
|
|
@@ -218,6 +220,30 @@ Règles de traitement rappelées à l'agent : une **idée** se rattache à une t
|
|
|
218
220
|
|
|
219
221
|
**Portée** : une demande vise l'espace où elle est écrite (l'espace ouvert dans l'interface) ; une review de test vise donc l'espace de sa tâche, une promotion d'idée l'espace de l'idée. Un jeton couvre un espace : son agent voit et traite les demandes de tous les membres de cet espace, avec les droits du propriétaire du jeton, et seulement là. Une demande prise par un jeton n'est traitée que par lui ; si l'agent s'arrête en cours de route, un membre la « Libère » depuis l'interface. Le jeton est revérifié à chaque appel ; quand son propriétaire quitte l'espace ou en est retiré, ses jetons sur cet espace sont révoqués et refusés dès l'appel suivant.
|
|
220
222
|
|
|
223
|
+
### Notifications de l'agent
|
|
224
|
+
|
|
225
|
+
L'agent peut être prévenu en temps réel des demandes qui passent en attente, sur le modèle de mcp-messenger. Le site ne pousse rien : un veilleur, sur la machine de l'agent, l'interroge en attente longue, et chaque ligne qu'il écrit réveille l'agent.
|
|
226
|
+
|
|
227
|
+
1. En début de session, l'agent appelle `enable_notifications`. Le mode MCP lit sur le site le jeton, son espace, le nombre de demandes en attente et l'état du veilleur (`GET notifications`), écrit le **relais** puis rend l'appel `Monitor` exact : commande `npx -y @imenam/mcp-organizer --watch <id du jeton>`, `timeout_ms` 1 800 000.
|
|
228
|
+
2. L'agent lance cette commande avec `Monitor`. Le veilleur est un processus du shell de la session, pas un enfant du mode MCP : il ne reçoit pas le bloc `env` du `.mcp.json`. L'identifiant du jeton, passé en argument, est public (ce n'est pas sa valeur) : il sert seulement à retrouver le relais.
|
|
229
|
+
3. Le **relais** est un fichier `~/.mcp-organizer/watch/<sha256(id du jeton)>.json` en 0600, qui porte l'URL du site et le jeton ; la valeur du jeton n'apparaît donc jamais dans la conversation. Il est écrit par `enable_notifications`, jamais au démarrage, et supprimé à l'arrêt du mode MCP qui l'a écrit en dernier ; un relais de plus de 7 jours est purgé. `ORGANIZER_STATE_DIR` déplace le répertoire (`<ORGANIZER_STATE_DIR>/watch`), pour les tests. Sans relais, le veilleur refuse de démarrer et dit d'appeler `enable_notifications`.
|
|
230
|
+
4. Le veilleur appelle `GET requests/wait?watcher=true` en boucle : chaque appel reste ouvert jusqu'à 50 s et répond dès qu'une demande de l'espace passe en attente (création, ou libération par un membre). Le curseur rendu, repassé en `since`, rattrape les demandes arrivées entre deux appels. Une panne réseau ou du site est réessayée avec un délai de 1 s à 30 s.
|
|
231
|
+
5. Pour chaque demande, une ligne sur stdout : `[organizer] idea · Bob Martin — "Un mode sombre" · ORG-4 (<id de la demande>)` (type, auteur, extrait, tâche de contexte, identifiant). L'agent se réveille et la traite par la boucle de travail, en commençant par `claim_request`. Les demandes déjà en attente quand le veilleur démarre ne sont pas annoncées : `enable_notifications` en donne le nombre, `list_requests` les liste.
|
|
232
|
+
6. Un refus du site (jeton révoqué, expiré, propriétaire retiré…) arrête le veilleur après une dernière ligne `[organizer] notifications stopped: <raison>`.
|
|
233
|
+
|
|
234
|
+
L'hôte arrête un `Monitor` au bout de 30 minutes au plus : l'agent relance alors le même appel `Monitor`, sans rappeler `enable_notifications`.
|
|
235
|
+
|
|
236
|
+
**Dans une gateway du gateway manager** (mcp-http-gateway), le mode MCP tourne sur la machine de la gateway et le veilleur sur celle de l'agent : un relais écrit par le mode MCP serait hors de portée du veilleur. Organizer suit alors la convention de veille de mcp-http-gateway, sans rien changer à la gateway ; pour l'agent, la démarche reste la même (`enable_notifications`, puis la commande rendue, avec `Monitor`).
|
|
237
|
+
|
|
238
|
+
1. `enable_notifications` déclare la propriété réservée `_gateway_watch`. Le relais `--mcp` de la gateway la cache à l'agent et la remplit à chaque appel (`{ "version": 1 }`).
|
|
239
|
+
2. Quand elle est remplie, le mode MCP n'écrit aucun fichier : il lit le curseur de départ (`GET notifications`) et rend la spécification `_meta["gateway/watch"] = { "tool": "wait_for_requests", "arguments": { "watcher": true }, "cursor": "<curseur>", "description": "New requests for the AI in the organizer" }`.
|
|
240
|
+
3. Le relais écrit son fichier de veille sur la machine de l'agent (adresse et jeton de la gateway, route, outil, curseur) et rend à l'agent l'appel `Monitor` de son propre veilleur (`node <gateway>/server.js --watch <fichier>`) ; il répond lui-même « déjà actives » quand ce veilleur tourne.
|
|
241
|
+
4. Ce veilleur appelle `wait_for_requests` à travers la gateway, en boucle, avec `cursor` et `timeout_seconds: 50` ; le mode MCP le relaie en `GET requests/wait?since=<curseur>&watcher=true` et rend `_meta["gateway/events"] = { "cursor", "lines" }`. Les lignes sont celles du veilleur local.
|
|
242
|
+
|
|
243
|
+
Le jeton organizer reste sur la machine de la gateway. Le veilleur de la gateway ne s'arrête pas sur un refus : il l'annonce une fois (`[gateway-watch] … la veille échoue`) et réessaie.
|
|
244
|
+
|
|
245
|
+
**Présence** : le site tient en mémoire, par jeton, les attentes en cours de son veilleur, avec un délai de grâce de 2 minutes entre deux attentes. `enable_notifications` répond « already live » quand un veilleur est déjà armé (en armer un second doublerait chaque notification), et la page Agent de l'interface affiche « à l'écoute » à côté du jeton. Les attentes du veilleur de la gateway comptent aussi (`watcher: true`).
|
|
246
|
+
|
|
221
247
|
### API des agents
|
|
222
248
|
|
|
223
249
|
`/api/agent/*` sert le mode MCP ; n'importe quel client HTTP peut l'appeler avec un jeton.
|
|
@@ -225,6 +251,7 @@ Règles de traitement rappelées à l'agent : une **idée** se rattache à une t
|
|
|
225
251
|
- **Authentification** : `Authorization: Bearer org_…`, revérifiée à chaque requête (jeton non révoqué, non expiré, propriétaire actif et toujours membre de l'espace) ; le dernier usage du jeton est mis à jour. Les cookies sont ignorés et le contrôle d'origine des modifications ne s'applique pas : sans cookie, une page tierce ne peut rien faire au nom d'un utilisateur.
|
|
226
252
|
- **Codes** : 401 pour un jeton absent, inconnu, révoqué (y compris au départ de son propriétaire) ou expiré (avec `WWW-Authenticate: Bearer`) ; 403 pour un propriétaire inactif ou qui n'est plus membre, une tâche ou une idée d'un autre espace, ou une demande prise par un autre jeton ; 404 pour une demande, une tâche ou une route inconnue ; 409 pour un état qui interdit l'action (demande en attente ou close) ; 400 pour des paramètres invalides ; 500 pour une erreur interne, avec un message générique et une référence (voir « Sécurité »). Le corps d'une erreur est `{ "error": "<message qui dit quoi faire>" }`.
|
|
227
253
|
- **Réponses** : `{ "text": "…", "data": { … } }`. `text` est le résultat rédigé pour l'agent (celui que renvoie l'outil) ; `data` porte les mêmes informations structurées, validées par les schémas zod de `shared/schemas/agent-tools.schema.ts`, partagés par le site et le mode MCP.
|
|
254
|
+
- **Attente longue** : `GET requests/wait?timeout_seconds=<1-50>&since=<curseur>&watcher=true` rend les demandes de l'espace passées en attente depuis `since`, sinon attend la prochaine, au plus `timeout_seconds` (30 par défaut) ; `data` porte les demandes, le curseur suivant et `timedOut`. `watcher=true` compte l'attente dans la présence du veilleur du jeton. `GET notifications` donne le jeton, son espace, le nombre de demandes en attente et l'état du veilleur.
|
|
228
255
|
- **Paramètres** : les noms des arguments des outils (`request_id`, `after_task`…) ; dans le chemin pour la tâche (`:ref`), l'idée et la demande, dans la requête pour un `GET`, dans le corps JSON sinon.
|
|
229
256
|
|
|
230
257
|
## Configuration
|
|
@@ -371,11 +398,12 @@ Chaque mutation ajoute à la table `changes` une ligne par entité touchée (`se
|
|
|
371
398
|
```
|
|
372
399
|
src/index.ts ligne de commande : aide, version, .env, choix du mode ou de la commande
|
|
373
400
|
src/server/ mode site : configuration, serveur Hono, protections, limitation des connexions
|
|
374
|
-
src/mcp/ mode MCP (stdio) : instructions, définitions des outils, client HTTP du site
|
|
401
|
+
src/mcp/ mode MCP (stdio) : instructions, définitions des outils, client HTTP du site,
|
|
402
|
+
enable_notifications et wait_for_requests (notifications.ts), relais du veilleur (watch-handoff.ts)
|
|
375
403
|
src/agent-api/ routes /api/agent/* : logique des outils de l'agent, authentifiée par jeton
|
|
376
404
|
src/gui-api.ts routes /api/* de l'interface
|
|
377
405
|
src/cli/ --setup (setup.ts), --set-user (set-user.ts), --backup (backup.ts),
|
|
378
|
-
--claude-setup-mcp (setup-mcp.ts), saisies au terminal (prompt.ts)
|
|
406
|
+
--claude-setup-mcp (setup-mcp.ts), --watch (watch.ts), saisies au terminal (prompt.ts)
|
|
379
407
|
src/deploy/ fichiers écrits par --setup : .env fusionné (env-render.ts), unité systemd et bloc nginx (templates.ts)
|
|
380
408
|
src/core/ base SQLite, migrations, store, comptes, espaces, tâches, tests, pièces jointes,
|
|
381
409
|
notifications, e-mail, dictée, file de l'agent, jetons d'agent, historique, .env, journaux, entrée .mcp.json
|
|
@@ -390,7 +418,7 @@ e2e/ parcours Playwright sur le site réel ; captures dans e2e/
|
|
|
390
418
|
|
|
391
419
|
Les tests ne lisent jamais la configuration du développeur : `test/launch.ts`, partagé par les tests node et les parcours e2e, lance chaque processus avec `--dotenv=<fichier vide du répertoire temporaire>` et vide toutes les variables de configuration héritées du shell. Le site de test est lancé en `--server` sur un port libre de `127.0.0.1`, avec `ORGANIZER_PUBLIC_URL=http://127.0.0.1:<port>` ; les outils sont appelés par un client MCP réel sur le mode MCP, relié à ce site par un jeton créé par l'API de l'interface (`ORGANIZER_URL`, `ORGANIZER_TOKEN`). Un test vérifie, sur une copie du package dont le `.env` détourne le répertoire de données, que ce `.env` n'atteint pas un processus lancé par les tests (et qu'il l'atteindrait sans `--dotenv`).
|
|
392
420
|
|
|
393
|
-
**Tests node** : migrations, invariants, versions, journal des changements, connexions concurrentes ; comptes (amorçage avec nom obligatoire, `--set-user` et `--name`, connexion, sessions, liens d'accès, invitations, droits, e-mail) ; profil ; jetons d'agent (affichage unique, configuration du mode MCP, hash seul en base, révocation, expiration, perte d'appartenance, espace hors de portée) ; `--claude-setup-mcp` (entrée du mode MCP, `--url` et `--token` obligatoires, fusion, `--force`, options inconnues ou invalides, fichier illisible laissé intact) ; `--setup` (`.env`, unité systemd et bloc nginx écrits, premier compte créé, `.env` existant fusionné, publication sous un chemin, valeurs incohérentes ou manquantes refusées sans rien écrire, valeurs dotenv citées, site démarré depuis le `.env` écrit) ; `--backup` (copie de la base d'un site en service et de ses pièces jointes, restaurable ; destination existante refusée sans `--force` ; pièce jointe manquante qui ne laisse aucun fichier ; répertoire sans base refusé) ; `.env` et `--dotenv` ; tâches, tâches de test, pièces jointes, notifications, boîte à idées, dictée avec un serveur Groq simulé, historique et annulation ; chaque outil appelé par un client MCP du SDK sur le mode MCP relié à un site de test, jeton inconnu, révoqué ou expiré, site injoignable, espace hors portée, demande prise par un autre jeton, deux jetons de deux membres sur le même espace commun (prise atomique), mode MCP sans `ORGANIZER_URL` / `ORGANIZER_TOKEN`, mode MCP qui ne charge aucun module natif (crochet de résolution qui les bloque), `ORGANIZER_URL` en http refusée hors de la machine locale, redirection du site refusée sans y envoyer le jeton, `.env` du paquet ignoré par le mode MCP (lu avec `--dotenv`) ; commandes du site arrêtées avec la marche à suivre quand better-sqlite3 n'a pas de module natif (crochet de résolution qui simule l'échec) ; `list_requests` sur plus de 100 demandes closes (les plus récentes d'abord) ; jetons révoqués au départ ou au retrait de leur propriétaire, et migration qui révoque ceux des anciens membres ; erreurs internes au message générique ; sauvegarde privée pendant son écriture (droits observés hors Windows, test ignoré sous Windows) ; `--setup` qui refuse `--trust-proxy=oui` avec une écoute hors de la boucle locale, bloc nginx en `listen 443 ssl http2` ; API des agents en HTTP direct (401 et 403 explicites, cookies ignorés, écriture sans `Origin`, réponses validées par les schémas partagés) ; site : `--help` (qui cite `--setup` et `--backup`) et `--version`, refus de connexion sans compte avec les commandes d'une installation par clonage et du paquet npm, démarrage refusé sans `ORGANIZER_PUBLIC_URL` ou `PORT` ou avec un chemin de base incohérent, SPA et `<base href>`, cookie (`Path`, `Secure` en https), formulaires multipart et 413 ; protections : modification refusée sans origine ou d'une origine étrangère, en-têtes de sécurité, limitation des connexions par couple (IP, e-mail) et par IP, rafale de tentatives parallèles limitée, compte non bloqué par un tiers, `X-Forwarded-For` ignoré sans `ORGANIZER_TRUST_PROXY` et pris en compte avec.
|
|
421
|
+
**Tests node** : migrations, invariants, versions, journal des changements, connexions concurrentes ; comptes (amorçage avec nom obligatoire, `--set-user` et `--name`, connexion, sessions, liens d'accès, invitations, droits, e-mail) ; profil ; jetons d'agent (affichage unique, configuration du mode MCP, hash seul en base, révocation, expiration, perte d'appartenance, espace hors de portée) ; `--claude-setup-mcp` (entrée du mode MCP, `--url` et `--token` obligatoires, fusion, `--force`, options inconnues ou invalides, fichier illisible laissé intact) ; `--setup` (`.env`, unité systemd et bloc nginx écrits, premier compte créé, `.env` existant fusionné, publication sous un chemin, valeurs incohérentes ou manquantes refusées sans rien écrire, valeurs dotenv citées, site démarré depuis le `.env` écrit) ; `--backup` (copie de la base d'un site en service et de ses pièces jointes, restaurable ; destination existante refusée sans `--force` ; pièce jointe manquante qui ne laisse aucun fichier ; répertoire sans base refusé) ; `.env` et `--dotenv` ; tâches, tâches de test, pièces jointes, notifications, boîte à idées, dictée avec un serveur Groq simulé, historique et annulation ; chaque outil appelé par un client MCP du SDK sur le mode MCP relié à un site de test, jeton inconnu, révoqué ou expiré, site injoignable, espace hors portée, demande prise par un autre jeton, deux jetons de deux membres sur le même espace commun (prise atomique), mode MCP sans `ORGANIZER_URL` / `ORGANIZER_TOKEN`, mode MCP qui ne charge aucun module natif (crochet de résolution qui les bloque), `ORGANIZER_URL` en http refusée hors de la machine locale, redirection du site refusée sans y envoyer le jeton, `.env` du paquet ignoré par le mode MCP (lu avec `--dotenv`) ; commandes du site arrêtées avec la marche à suivre quand better-sqlite3 n'a pas de module natif (crochet de résolution qui simule l'échec) ; `list_requests` sur plus de 100 demandes closes (les plus récentes d'abord) ; jetons révoqués au départ ou au retrait de leur propriétaire, et migration qui révoque ceux des anciens membres ; erreurs internes au message générique ; sauvegarde privée pendant son écriture (droits observés hors Windows, test ignoré sous Windows) ; `--setup` qui refuse `--trust-proxy=oui` avec une écoute hors de la boucle locale, bloc nginx en `listen 443 ssl http2` ; notifications de l'agent (présence et délai de grâce du veilleur, ligne d'une demande, `GET notifications`, attente longue réveillée par une demande créée ou libérée, curseur qui rattrape une demande arrivée entre deux attentes, expiration, jeton inconnu, durée hors limites ; `enable_notifications` puis veilleur `--watch` réel qui écrit une ligne par nouvelle demande, sans le jeton dans la conversation, « already live » une fois armé, arrêt sur jeton révoqué, refus de démarrer sans relais ; convention de mcp-http-gateway : propriété `_gateway_watch`, spécification de veille sans relais local, curseur de départ qui rattrape une demande, lignes et curseur de `wait_for_requests`, présence, arguments invalides) ; API des agents en HTTP direct (401 et 403 explicites, cookies ignorés, écriture sans `Origin`, réponses validées par les schémas partagés) ; site : `--help` (qui cite `--setup` et `--backup`) et `--version`, refus de connexion sans compte avec les commandes d'une installation par clonage et du paquet npm, démarrage refusé sans `ORGANIZER_PUBLIC_URL` ou `PORT` ou avec un chemin de base incohérent, SPA et `<base href>`, cookie (`Path`, `Secure` en https), formulaires multipart et 413 ; protections : modification refusée sans origine ou d'une origine étrangère, en-têtes de sécurité, limitation des connexions par couple (IP, e-mail) et par IP, rafale de tentatives parallèles limitée, compte non bloqué par un tiers, `X-Forwarded-For` ignoré sans `ORGANIZER_TRUST_PROXY` et pris en compte avec.
|
|
394
422
|
|
|
395
423
|
**Parcours e2e** (site compilé en `--server`, port libre, répertoire de données temporaire, premier compte créé par `ORGANIZER_ADMIN_*`). Chaque page est surveillée : une exception ou une erreur de console, dont toute violation de la CSP, fait échouer le parcours.
|
|
396
424
|
|
|
@@ -398,7 +426,7 @@ Les tests ne lisent jamais la configuration du développeur : `test/launch.ts`,
|
|
|
398
426
|
- connexion refusée puis réussie, création d'un espace commun, invitation, activation du compte invité par le lien dans un second navigateur ; invitation d'un second espace rejointe depuis « Invitations reçues », ancien lien refusé ;
|
|
399
427
|
- tâches, sous-tâches et sous-sous-tâches, glisser-déposer entre colonnes et dans une colonne (persisté), assignation notifiée chez l'assignée ;
|
|
400
428
|
- tâche de test à deux testeurs, résultats, review soumise qui crée une demande traitée par l'agent ; testeuse désassignée puis retirée, résultats conservés en lecture seule ;
|
|
401
|
-
- idée saisie dans l'interface → demande en attente → traitement par un client MCP réel sur le mode MCP, avec un jeton créé par l'API de l'interface (tâche créée, statut et priorité changés) → réponse, actions, jeton qui a traité la demande et dernier passage de l'agent visibles → annulation d'une action puis du lot, tâche retirée du tableau de bord ; demande restée en cours libérée depuis la page Agent puis reprise ;
|
|
429
|
+
- idée saisie dans l'interface → demande en attente → traitement par un client MCP réel sur le mode MCP, avec un jeton créé par l'API de l'interface (tâche créée, statut et priorité changés) → réponse, actions, jeton qui a traité la demande et dernier passage de l'agent visibles → annulation d'une action puis du lot, tâche retirée du tableau de bord ; demande restée en cours libérée depuis la page Agent puis reprise ; jeton dont le veilleur attend affiché « à l'écoute », réveillé par une demande ;
|
|
402
430
|
- portée : les agents de Julie (un jeton sur son espace personnel, un sur l'espace commun) ne voient que les demandes de leur espace et ne lisent ni ne modifient l'espace personnel de Matthieu, sur lequel elle ne peut pas créer de jeton ;
|
|
403
431
|
- panneau assistant, « Modifiée par l'agent · Annuler » et restauration d'une version ;
|
|
404
432
|
- boîte à idées : promotion manuelle et promotion confiée à l'agent ;
|
|
@@ -425,7 +453,7 @@ Thème sombre, épuré, une seule action principale par écran. L'interface se r
|
|
|
425
453
|
| **Détail d'un test** | Même gabarit avec « Test », la ligne « Teste ORG-41 … », la barre de bilan et « 9/12 vérifiés · 2 KO », un onglet par testeur, puis un onglet en lecture seule par ancien testeur (désassigné ou parti de l'espace), un résultat par point (OK, KO, Bloqué, À vérifier, avec commentaire), « Ma review » avec dictée et « Soumettre la review », reviews des autres testeurs repliées avec l'état de leur demande. |
|
|
426
454
|
| **Saisir une idée** | Modale : texte, micro, « Envoyer » ; « L'agent la classera à son prochain passage ». |
|
|
427
455
|
| **Assistant** | Panneau latéral : contexte « À propos de ORG-41 » (tâche ouverte), fil des demandes de l'utilisateur et des réponses de l'agent, statut, « Modifier · Annuler » en attente, « Libérer » en cours, nom du jeton qui traite ou a traité la demande (son propriétaire en infobulle), « N actions · Annuler ». |
|
|
428
|
-
| **Agent** (`#/agent`) | État des agents de l'espace : chaque jeton actif qui le couvre, son propriétaire et son dernier passage (point allumé s'il date de moins de 10 min), ou « Aucun agent sur cet espace · Ajouter un agent » (lien vers Paramètres) ; « Filtrer » ; demandes En attente (crayon et croix au survol), En cours (jeton qui l'a prise, « Libérer »), Traitées (jeton qui l'a traitée ; elles se déplient : réponse, actions avec « Annuler », « Tout annuler »), Annulées repliées. |
|
|
456
|
+
| **Agent** (`#/agent`) | État des agents de l'espace, rechargé toutes les 30 s : chaque jeton actif qui le couvre, son propriétaire et son dernier passage (point allumé s'il date de moins de 10 min), ou « à l'écoute » quand son veilleur de notifications est armé, ou « Aucun agent sur cet espace · Ajouter un agent » (lien vers Paramètres) ; « Filtrer » ; demandes En attente (crayon et croix au survol), En cours (jeton qui l'a prise, « Libérer »), Traitées (jeton qui l'a traitée ; elles se déplient : réponse, actions avec « Annuler », « Tout annuler »), Annulées repliées. |
|
|
429
457
|
| **Idées** (`#/idees`) | Liste avec ajout rapide, idées classées par l'agent, « Promouvoir » ; tiroir « Promouvoir en tâche » : « Laisser l'agent la formater », ou « la remplir moi-même » (titre, type, priorité). |
|
|
430
458
|
| **Membres** | Modale : invitation par adresse, membres et rôles, invitations en attente (renvoyer, révoquer, lien à copier quand l'e-mail ne part pas), renommer ou supprimer l'espace. |
|
|
431
459
|
| **Invitations reçues** | Modale ouverte depuis le menu du sélecteur d'espace : pour chaque espace qui invite l'utilisateur, l'auteur de l'invitation, « Refuser » et « Rejoindre » (qui ouvre l'espace). |
|
|
@@ -234,6 +234,7 @@ export declare const RequestListResponseSchema: z.ZodObject<{
|
|
|
234
234
|
name: z.ZodString;
|
|
235
235
|
ownerName: z.ZodString;
|
|
236
236
|
lastUsedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
237
|
+
listening: z.ZodBoolean;
|
|
237
238
|
}, z.core.$strip>>;
|
|
238
239
|
}, z.core.$strip>;
|
|
239
240
|
export type RequestListResponse = z.infer<typeof RequestListResponseSchema>;
|
|
@@ -75,8 +75,11 @@ export const RequestListResponseSchema = z.object({
|
|
|
75
75
|
/** Demandes de l'espace, de la plus récente à la plus ancienne. */
|
|
76
76
|
requests: z.array(RequestViewSchema),
|
|
77
77
|
pendingCount: z.number().int().min(0),
|
|
78
|
-
/**
|
|
79
|
-
|
|
78
|
+
/**
|
|
79
|
+
* Jetons actifs qui couvrent l'espace, du plus récemment utilisé au moins
|
|
80
|
+
* récemment ; `listening` : un veilleur de notifications de ce jeton est armé.
|
|
81
|
+
*/
|
|
82
|
+
agents: z.array(AgentRefSchema.extend({ lastUsedAt: TimestampSchema.nullable(), listening: z.boolean() })),
|
|
80
83
|
});
|
|
81
84
|
/** Version d'une tâche dans son historique. */
|
|
82
85
|
export const TaskVersionViewSchema = z.object({
|
|
@@ -25,6 +25,20 @@ export declare const ListRequestsQuerySchema: z.ZodObject<{
|
|
|
25
25
|
}>>;
|
|
26
26
|
limit: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
27
27
|
}, z.core.$strict>;
|
|
28
|
+
/** Durée maximale d'une attente longue de `GET requests/wait`, en secondes. */
|
|
29
|
+
export declare const MAX_WAIT_SECONDS = 50;
|
|
30
|
+
/**
|
|
31
|
+
* `GET requests/wait` : durée de l'attente ; `since`, curseur rendu par
|
|
32
|
+
* l'attente précédente (les demandes passées en attente depuis sont rendues
|
|
33
|
+
* aussitôt) ; `watcher=true` pour un veilleur de notifications.
|
|
34
|
+
*/
|
|
35
|
+
export declare const WaitRequestsQuerySchema: z.ZodObject<{
|
|
36
|
+
timeout_seconds: z.ZodDefault<z.ZodCoercedNumber<unknown>>;
|
|
37
|
+
since: z.ZodOptional<z.ZodISODateTime>;
|
|
38
|
+
watcher: z.ZodOptional<z.ZodEnum<{
|
|
39
|
+
true: "true";
|
|
40
|
+
}>>;
|
|
41
|
+
}, z.core.$strict>;
|
|
28
42
|
/** `POST requests/:id/claim` : sans corps. */
|
|
29
43
|
export declare const ClaimRequestBodySchema: z.ZodObject<{}, z.core.$strict>;
|
|
30
44
|
export declare const CompleteRequestBodySchema: z.ZodObject<{
|
|
@@ -722,3 +736,88 @@ export declare const PromoteIdeaDataSchema: z.ZodObject<{
|
|
|
722
736
|
tags: z.ZodArray<z.ZodString>;
|
|
723
737
|
}, z.core.$strip>;
|
|
724
738
|
}, z.core.$strip>;
|
|
739
|
+
/** Présence d'un veilleur de notifications pour le jeton. */
|
|
740
|
+
export declare const AgentWatcherSchema: z.ZodObject<{
|
|
741
|
+
armed: z.ZodBoolean;
|
|
742
|
+
since: z.ZodNullable<z.ZodISODateTime>;
|
|
743
|
+
lastSeen: z.ZodNullable<z.ZodISODateTime>;
|
|
744
|
+
}, z.core.$strip>;
|
|
745
|
+
/**
|
|
746
|
+
* `GET notifications` : jeton et espace de l'agent, demandes en attente,
|
|
747
|
+
* veilleur du jeton et curseur de départ d'une veille (`since` de la
|
|
748
|
+
* première attente : seules les demandes passées en attente ensuite sont rendues).
|
|
749
|
+
*/
|
|
750
|
+
export declare const NotificationsDataSchema: z.ZodObject<{
|
|
751
|
+
token: z.ZodObject<{
|
|
752
|
+
id: z.ZodString;
|
|
753
|
+
name: z.ZodString;
|
|
754
|
+
}, z.core.$strip>;
|
|
755
|
+
space: z.ZodObject<{
|
|
756
|
+
id: z.ZodString;
|
|
757
|
+
name: z.ZodString;
|
|
758
|
+
kind: z.ZodEnum<{
|
|
759
|
+
personal: "personal";
|
|
760
|
+
shared: "shared";
|
|
761
|
+
}>;
|
|
762
|
+
keyPrefix: z.ZodString;
|
|
763
|
+
}, z.core.$strip>;
|
|
764
|
+
pendingCount: z.ZodNumber;
|
|
765
|
+
watcher: z.ZodObject<{
|
|
766
|
+
armed: z.ZodBoolean;
|
|
767
|
+
since: z.ZodNullable<z.ZodISODateTime>;
|
|
768
|
+
lastSeen: z.ZodNullable<z.ZodISODateTime>;
|
|
769
|
+
}, z.core.$strip>;
|
|
770
|
+
cursor: z.ZodISODateTime;
|
|
771
|
+
}, z.core.$strip>;
|
|
772
|
+
/**
|
|
773
|
+
* `GET requests/wait` : demandes passées en attente (vide à l'expiration) et
|
|
774
|
+
* curseur à repasser en `since` à l'attente suivante.
|
|
775
|
+
*/
|
|
776
|
+
export declare const WaitRequestsDataSchema: z.ZodObject<{
|
|
777
|
+
space: z.ZodObject<{
|
|
778
|
+
id: z.ZodString;
|
|
779
|
+
name: z.ZodString;
|
|
780
|
+
kind: z.ZodEnum<{
|
|
781
|
+
personal: "personal";
|
|
782
|
+
shared: "shared";
|
|
783
|
+
}>;
|
|
784
|
+
keyPrefix: z.ZodString;
|
|
785
|
+
}, z.core.$strip>;
|
|
786
|
+
requests: z.ZodArray<z.ZodObject<{
|
|
787
|
+
id: z.ZodString;
|
|
788
|
+
kind: z.ZodEnum<{
|
|
789
|
+
idea: "idea";
|
|
790
|
+
idea_promotion: "idea_promotion";
|
|
791
|
+
message: "message";
|
|
792
|
+
remark: "remark";
|
|
793
|
+
test_review: "test_review";
|
|
794
|
+
}>;
|
|
795
|
+
status: z.ZodEnum<{
|
|
796
|
+
cancelled: "cancelled";
|
|
797
|
+
done: "done";
|
|
798
|
+
failed: "failed";
|
|
799
|
+
pending: "pending";
|
|
800
|
+
processing: "processing";
|
|
801
|
+
}>;
|
|
802
|
+
text: z.ZodString;
|
|
803
|
+
author: z.ZodObject<{
|
|
804
|
+
id: z.ZodString;
|
|
805
|
+
name: z.ZodString;
|
|
806
|
+
email: z.ZodString;
|
|
807
|
+
}, z.core.$strip>;
|
|
808
|
+
contextTask: z.ZodNullable<z.ZodObject<{
|
|
809
|
+
id: z.ZodString;
|
|
810
|
+
key: z.ZodString;
|
|
811
|
+
title: z.ZodString;
|
|
812
|
+
}, z.core.$strip>>;
|
|
813
|
+
contextIdeaId: z.ZodNullable<z.ZodString>;
|
|
814
|
+
createdAt: z.ZodISODateTime;
|
|
815
|
+
claimedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
816
|
+
claimedByThisToken: z.ZodBoolean;
|
|
817
|
+
reply: z.ZodNullable<z.ZodString>;
|
|
818
|
+
failureReason: z.ZodNullable<z.ZodString>;
|
|
819
|
+
}, z.core.$strip>>;
|
|
820
|
+
cursor: z.ZodISODateTime;
|
|
821
|
+
timedOut: z.ZodBoolean;
|
|
822
|
+
}, z.core.$strip>;
|
|
823
|
+
export type WaitRequestsData = z.infer<typeof WaitRequestsDataSchema>;
|
|
@@ -26,6 +26,20 @@ export const ListRequestsQuerySchema = z
|
|
|
26
26
|
limit: z.coerce.number().int().min(1).max(MAX_REQUEST_LIST_LIMIT).default(20),
|
|
27
27
|
})
|
|
28
28
|
.strict();
|
|
29
|
+
/** Durée maximale d'une attente longue de `GET requests/wait`, en secondes. */
|
|
30
|
+
export const MAX_WAIT_SECONDS = 50;
|
|
31
|
+
/**
|
|
32
|
+
* `GET requests/wait` : durée de l'attente ; `since`, curseur rendu par
|
|
33
|
+
* l'attente précédente (les demandes passées en attente depuis sont rendues
|
|
34
|
+
* aussitôt) ; `watcher=true` pour un veilleur de notifications.
|
|
35
|
+
*/
|
|
36
|
+
export const WaitRequestsQuerySchema = z
|
|
37
|
+
.object({
|
|
38
|
+
timeout_seconds: z.coerce.number().int().min(1).max(MAX_WAIT_SECONDS).default(30),
|
|
39
|
+
since: TimestampSchema.optional(),
|
|
40
|
+
watcher: z.enum(["true"]).optional(),
|
|
41
|
+
})
|
|
42
|
+
.strict();
|
|
29
43
|
/** `POST requests/:id/claim` : sans corps. */
|
|
30
44
|
export const ClaimRequestBodySchema = z.object({}).strict();
|
|
31
45
|
export const CompleteRequestBodySchema = z.object({ reply: z.string().max(MAX_REPLY_LENGTH) }).strict();
|
|
@@ -163,3 +177,27 @@ export const ChecklistDataSchema = z.object({
|
|
|
163
177
|
export const CommentDataSchema = z.object({ task: AgentTaskSummarySchema, commentId: IdSchema });
|
|
164
178
|
export const IdeaDataSchema = z.object({ idea: AgentIdeaSchema });
|
|
165
179
|
export const PromoteIdeaDataSchema = z.object({ idea: AgentIdeaSchema, task: AgentTaskSummarySchema });
|
|
180
|
+
/** Présence d'un veilleur de notifications pour le jeton. */
|
|
181
|
+
export const AgentWatcherSchema = z.object({ armed: z.boolean(), since: TimestampSchema.nullable(), lastSeen: TimestampSchema.nullable() });
|
|
182
|
+
/**
|
|
183
|
+
* `GET notifications` : jeton et espace de l'agent, demandes en attente,
|
|
184
|
+
* veilleur du jeton et curseur de départ d'une veille (`since` de la
|
|
185
|
+
* première attente : seules les demandes passées en attente ensuite sont rendues).
|
|
186
|
+
*/
|
|
187
|
+
export const NotificationsDataSchema = z.object({
|
|
188
|
+
token: z.object({ id: IdSchema, name: z.string() }),
|
|
189
|
+
space: AgentSpaceSchema,
|
|
190
|
+
pendingCount: z.number().int().min(0),
|
|
191
|
+
watcher: AgentWatcherSchema,
|
|
192
|
+
cursor: TimestampSchema,
|
|
193
|
+
});
|
|
194
|
+
/**
|
|
195
|
+
* `GET requests/wait` : demandes passées en attente (vide à l'expiration) et
|
|
196
|
+
* curseur à repasser en `since` à l'attente suivante.
|
|
197
|
+
*/
|
|
198
|
+
export const WaitRequestsDataSchema = z.object({
|
|
199
|
+
space: AgentSpaceSchema,
|
|
200
|
+
requests: z.array(AgentRequestSummarySchema),
|
|
201
|
+
cursor: TimestampSchema,
|
|
202
|
+
timedOut: z.boolean(),
|
|
203
|
+
});
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type AgentApiContext, type AgentApiRequest, type AgentApiResponse, type AgentRoute } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Notifications de l'agent : état (`GET notifications`) et attente longue
|
|
4
|
+
* des demandes qui passent en attente (`GET requests/wait`), appelée en
|
|
5
|
+
* boucle par le veilleur que lance `enable_notifications`.
|
|
6
|
+
*/
|
|
7
|
+
export declare const notificationRoutes: AgentRoute[];
|
|
8
|
+
/**
|
|
9
|
+
* `GET requests/wait` : rend aussitôt les demandes passées en attente depuis
|
|
10
|
+
* `since`, sinon attend qu'une demande de l'espace du jeton passe en attente
|
|
11
|
+
* (création ou libération), au plus `timeout_seconds`. Le curseur rendu est
|
|
12
|
+
* l'horodatage de la dernière demande rendue, ou le point de départ de
|
|
13
|
+
* l'attente à l'expiration. Une attente `watcher=true` compte dans la
|
|
14
|
+
* présence du veilleur du jeton.
|
|
15
|
+
*/
|
|
16
|
+
export declare function waitForPendingRequests(ctx: AgentApiContext, request: AgentApiRequest, signal: AbortSignal): Promise<AgentApiResponse>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { MAX_REQUEST_LIST_LIMIT, NoQuerySchema, NotificationsDataSchema, WaitRequestsDataSchema, WaitRequestsQuerySchema, } from "../../shared/schemas/agent-tools.schema.js";
|
|
2
|
+
import { resolveAgentToken } from "../core/agent-tokens.js";
|
|
3
|
+
import { requestData, requestLine, spaceData } from "../core/agent.js";
|
|
4
|
+
import { parseInput, respond } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Notifications de l'agent : état (`GET notifications`) et attente longue
|
|
7
|
+
* des demandes qui passent en attente (`GET requests/wait`), appelée en
|
|
8
|
+
* boucle par le veilleur que lance `enable_notifications`.
|
|
9
|
+
*/
|
|
10
|
+
export const notificationRoutes = [
|
|
11
|
+
{
|
|
12
|
+
method: "GET",
|
|
13
|
+
path: "/notifications",
|
|
14
|
+
handle: ({ store }, { agent, query }) => {
|
|
15
|
+
parseInput(NoQuerySchema, query);
|
|
16
|
+
const watcher = store.watchers.status(agent.token.id);
|
|
17
|
+
const pendingCount = store.countPendingRequestsOfSpace(agent.space.id);
|
|
18
|
+
const state = watcher.armed ? `a watcher is armed for this token since ${watcher.since}` : "no watcher is armed for this token";
|
|
19
|
+
return respond(NotificationsDataSchema, `Space "${agent.space.name}": ${pendingCount} pending request(s); ${state}.`, {
|
|
20
|
+
token: { id: agent.token.id, name: agent.token.name },
|
|
21
|
+
space: spaceData(agent.space),
|
|
22
|
+
pendingCount,
|
|
23
|
+
watcher,
|
|
24
|
+
cursor: new Date().toISOString(),
|
|
25
|
+
});
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
];
|
|
29
|
+
/** Demandes en attente de l'espace du jeton modifiées strictement après `since`. */
|
|
30
|
+
function pendingSince(ctx, agent, since) {
|
|
31
|
+
return ctx.store.listAgentRequestsTargeting(agent.space.id, "pending", MAX_REQUEST_LIST_LIMIT).filter((request) => request.updatedAt > since);
|
|
32
|
+
}
|
|
33
|
+
function waitResponse(ctx, agent, requests, cursor, timeoutSeconds) {
|
|
34
|
+
const data = {
|
|
35
|
+
space: spaceData(agent.space),
|
|
36
|
+
requests: requests.map((request) => requestData(ctx.store, agent, request)),
|
|
37
|
+
cursor,
|
|
38
|
+
timedOut: requests.length === 0,
|
|
39
|
+
};
|
|
40
|
+
if (requests.length === 0)
|
|
41
|
+
return respond(WaitRequestsDataSchema, `No new pending request in "${agent.space.name}" after ${timeoutSeconds}s.`, data);
|
|
42
|
+
const lines = requests.map((request) => requestLine(ctx.store, agent, request)).join("\n");
|
|
43
|
+
return respond(WaitRequestsDataSchema, `${requests.length} new pending request(s) in "${agent.space.name}":\n${lines}\n\nNext: call claim_request with the id of the oldest request.`, data);
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* `GET requests/wait` : rend aussitôt les demandes passées en attente depuis
|
|
47
|
+
* `since`, sinon attend qu'une demande de l'espace du jeton passe en attente
|
|
48
|
+
* (création ou libération), au plus `timeout_seconds`. Le curseur rendu est
|
|
49
|
+
* l'horodatage de la dernière demande rendue, ou le point de départ de
|
|
50
|
+
* l'attente à l'expiration. Une attente `watcher=true` compte dans la
|
|
51
|
+
* présence du veilleur du jeton.
|
|
52
|
+
*/
|
|
53
|
+
export function waitForPendingRequests(ctx, request, signal) {
|
|
54
|
+
const agent = resolveAgentToken(ctx.store, request.bearer, ctx.now?.() ?? new Date());
|
|
55
|
+
const input = parseInput(WaitRequestsQuerySchema, request.query);
|
|
56
|
+
const startedAt = new Date().toISOString();
|
|
57
|
+
const since = input.since ?? null;
|
|
58
|
+
if (since !== null) {
|
|
59
|
+
const ready = pendingSince(ctx, agent, since);
|
|
60
|
+
if (ready.length > 0)
|
|
61
|
+
return Promise.resolve(waitResponse(ctx, agent, ready, ready.map((r) => r.updatedAt).sort().at(-1), input.timeout_seconds));
|
|
62
|
+
}
|
|
63
|
+
return new Promise((resolve) => {
|
|
64
|
+
const releaseWatcher = input.watcher ? ctx.store.watchers.begin(agent.token.id) : () => { };
|
|
65
|
+
const received = [];
|
|
66
|
+
let flush = null;
|
|
67
|
+
const stop = () => {
|
|
68
|
+
clearTimeout(timer);
|
|
69
|
+
if (flush)
|
|
70
|
+
clearImmediate(flush);
|
|
71
|
+
unsubscribe();
|
|
72
|
+
signal.removeEventListener("abort", onAbort);
|
|
73
|
+
releaseWatcher();
|
|
74
|
+
};
|
|
75
|
+
const finish = (requests, cursor) => {
|
|
76
|
+
stop();
|
|
77
|
+
resolve(waitResponse(ctx, agent, requests, cursor, input.timeout_seconds));
|
|
78
|
+
};
|
|
79
|
+
// Les demandes d'une même mutation arrivent ensemble : elles partent dans la même réponse.
|
|
80
|
+
const unsubscribe = ctx.store.onPendingRequest((pending) => {
|
|
81
|
+
if (pending.targetSpaceId !== agent.space.id || pending.status !== "pending")
|
|
82
|
+
return;
|
|
83
|
+
received.push(pending);
|
|
84
|
+
flush ??= setImmediate(() => finish(received, received.map((r) => r.updatedAt).sort().at(-1)));
|
|
85
|
+
});
|
|
86
|
+
const timer = setTimeout(() => finish([], since ?? startedAt), input.timeout_seconds * 1000);
|
|
87
|
+
const onAbort = () => stop();
|
|
88
|
+
signal.addEventListener("abort", onAbort);
|
|
89
|
+
if (signal.aborted)
|
|
90
|
+
stop();
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -2,6 +2,7 @@ import { resolveAgentToken } from "../core/agent-tokens.js";
|
|
|
2
2
|
import { NotFoundError, OrganizerError } from "../core/errors.js";
|
|
3
3
|
import { checklistRoutes } from "./checklist.js";
|
|
4
4
|
import { ideaRoutes } from "./ideas.js";
|
|
5
|
+
import { notificationRoutes } from "./notifications.js";
|
|
5
6
|
import { requestRoutes } from "./requests.js";
|
|
6
7
|
import { taskRoutes } from "./tasks.js";
|
|
7
8
|
function compile(route) {
|
|
@@ -12,7 +13,7 @@ function compile(route) {
|
|
|
12
13
|
});
|
|
13
14
|
return { route, pattern: new RegExp(`^${source}$`), keys };
|
|
14
15
|
}
|
|
15
|
-
const ROUTES = [...requestRoutes, ...taskRoutes, ...checklistRoutes, ...ideaRoutes].map(compile);
|
|
16
|
+
const ROUTES = [...requestRoutes, ...taskRoutes, ...checklistRoutes, ...ideaRoutes, ...notificationRoutes].map(compile);
|
|
16
17
|
/** Segment de chemin décodé ; un encodage `%` invalide est une erreur 400. */
|
|
17
18
|
function decodeSegment(raw) {
|
|
18
19
|
try {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function runWatchMode(tokenId: string | undefined): Promise<number>;
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { MAX_WAIT_SECONDS } from "../../shared/schemas/agent-tools.schema.js";
|
|
2
|
+
import { ConfigError } from "../core/errors.js";
|
|
3
|
+
import { OrganizerSiteClient, SiteError } from "../mcp/client.js";
|
|
4
|
+
import { formatRequestEvent } from "../mcp/notifications.js";
|
|
5
|
+
import { readHandoff } from "../mcp/watch-handoff.js";
|
|
6
|
+
/**
|
|
7
|
+
* Mode veilleur (`--watch <identifiant du jeton>`).
|
|
8
|
+
*
|
|
9
|
+
* Une ligne sur stdout = une demande passée en attente. Ce flux est consommé
|
|
10
|
+
* par l'outil `Monitor` de l'hôte, qui réveille la session de l'agent à chaque
|
|
11
|
+
* ligne, y compris quand elle est inactive. Le journal part sur stderr.
|
|
12
|
+
*
|
|
13
|
+
* Une panne réseau ou du site est réessayée avec un délai croissant. Un refus
|
|
14
|
+
* du site (jeton révoqué, expiré, route inconnue) arrête le veilleur après une
|
|
15
|
+
* dernière ligne qui en donne la raison.
|
|
16
|
+
*/
|
|
17
|
+
const BACKOFF_MIN_MS = 1_000;
|
|
18
|
+
const BACKOFF_MAX_MS = 30_000;
|
|
19
|
+
/** Refus définitif du site : un nouvel essai donnerait la même réponse. */
|
|
20
|
+
function isRefusal(error) {
|
|
21
|
+
return error instanceof SiteError && error.status !== undefined && error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429;
|
|
22
|
+
}
|
|
23
|
+
export async function runWatchMode(tokenId) {
|
|
24
|
+
if (!tokenId || tokenId.startsWith("--")) {
|
|
25
|
+
throw new ConfigError("--watch needs the token id given by the enable_notifications tool: `mcp-organizer --watch <token id>`.");
|
|
26
|
+
}
|
|
27
|
+
const handoff = readHandoff(tokenId);
|
|
28
|
+
if (!handoff) {
|
|
29
|
+
throw new ConfigError(`No notification handoff for token ${tokenId}: call the enable_notifications tool of the organizer MCP server, then arm the watcher with the command it returns.`);
|
|
30
|
+
}
|
|
31
|
+
const client = new OrganizerSiteClient({ url: handoff.url, token: handoff.token });
|
|
32
|
+
process.stderr.write(`watching token ${tokenId} on ${handoff.url}\n`);
|
|
33
|
+
let since;
|
|
34
|
+
let backoff = BACKOFF_MIN_MS;
|
|
35
|
+
for (;;) {
|
|
36
|
+
try {
|
|
37
|
+
const result = await client.waitForRequests({ timeout_seconds: MAX_WAIT_SECONDS, since, watcher: true });
|
|
38
|
+
backoff = BACKOFF_MIN_MS;
|
|
39
|
+
since = result.cursor;
|
|
40
|
+
for (const request of result.requests)
|
|
41
|
+
process.stdout.write(`${formatRequestEvent(request)}\n`);
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
45
|
+
if (isRefusal(error)) {
|
|
46
|
+
process.stdout.write(`[organizer] notifications stopped: ${detail.replace(/\s+/g, " ")}\n`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
process.stderr.write(`wait failed, retrying in ${Math.round(backoff / 1000)}s: ${detail}\n`);
|
|
50
|
+
await new Promise((resolve) => setTimeout(resolve, backoff));
|
|
51
|
+
backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -40,7 +40,8 @@ export declare function releaseRequest(store: Store, userId: string, requestId:
|
|
|
40
40
|
export declare function getRequest(store: Store, userId: string, requestId: string): RequestView;
|
|
41
41
|
/**
|
|
42
42
|
* Demandes d'un espace, avec le nombre de demandes en attente et les jetons
|
|
43
|
-
* actifs qui couvrent l'espace, chacun avec son dernier usage
|
|
43
|
+
* actifs qui couvrent l'espace, chacun avec son dernier usage et la présence
|
|
44
|
+
* de son veilleur de notifications.
|
|
44
45
|
*/
|
|
45
46
|
export declare function listRequests(store: Store, userId: string, spaceId: string, now?: Date): RequestListResponse;
|
|
46
47
|
/** Annule une action de l'agent. */
|
|
@@ -190,7 +190,8 @@ export function getRequest(store, userId, requestId) {
|
|
|
190
190
|
}
|
|
191
191
|
/**
|
|
192
192
|
* Demandes d'un espace, avec le nombre de demandes en attente et les jetons
|
|
193
|
-
* actifs qui couvrent l'espace, chacun avec son dernier usage
|
|
193
|
+
* actifs qui couvrent l'espace, chacun avec son dernier usage et la présence
|
|
194
|
+
* de son veilleur de notifications.
|
|
194
195
|
*/
|
|
195
196
|
export function listRequests(store, userId, spaceId, now = new Date()) {
|
|
196
197
|
requireMember(store, spaceId, userId);
|
|
@@ -198,7 +199,7 @@ export function listRequests(store, userId, spaceId, now = new Date()) {
|
|
|
198
199
|
const agents = store
|
|
199
200
|
.listAgentTokensOfSpace(spaceId)
|
|
200
201
|
.filter((token) => tokenState(store, token, now) === "active")
|
|
201
|
-
.map((token) => ({ ...agentRef(store, token), lastUsedAt: token.lastUsedAt }));
|
|
202
|
+
.map((token) => ({ ...agentRef(store, token), lastUsedAt: token.lastUsedAt, listening: store.watchers.status(token.id).armed }));
|
|
202
203
|
return { requests, pendingCount: store.countPendingRequestsOfSpace(spaceId), agents };
|
|
203
204
|
}
|
|
204
205
|
// --- Annulation -----------------------------------------------------------------
|
package/dist/src/core/store.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type Database from "better-sqlite3";
|
|
2
|
+
import { WatcherRegistry } from "./watchers.js";
|
|
2
3
|
import { type EntityType, type MemberRole, type RequestKind, type RequestStatus, type SpaceKind, type TaskPriority, type TaskStatus, type TaskType, type TestResultValue, type AccessLinkPurpose, type AgentOperation, type UserStatus, type VersionedEntityType } from "../../shared/schemas/enums.schema.js";
|
|
3
4
|
import { type AccessLink, type AgentAction, type AgentRequest, type AgentToken, type Attachment, type Change, type ChecklistItem, type Comment, type EntityVersion, type Idea, type Invitation, type Membership, type Notification, type Session, type Space, type Tag, type Task, type TaskSnapshot, type TestCheckpoint, type TestResult, type TestReview, type User, type UserSettings } from "../../shared/schemas/entities.schema.js";
|
|
4
5
|
/**
|
|
@@ -24,6 +25,9 @@ export declare class Store {
|
|
|
24
25
|
readonly db: Database.Database;
|
|
25
26
|
private activeMutation;
|
|
26
27
|
private emailListener;
|
|
28
|
+
private readonly pendingRequestListeners;
|
|
29
|
+
/** Présence des veilleurs de notifications, par jeton d'agent. */
|
|
30
|
+
readonly watchers: WatcherRegistry;
|
|
27
31
|
constructor(db: Database.Database);
|
|
28
32
|
/** Ouvre (et migre) la base du répertoire de données. */
|
|
29
33
|
static open(dataDir: string): Store;
|
|
@@ -180,6 +184,12 @@ export declare class Store {
|
|
|
180
184
|
* créées.
|
|
181
185
|
*/
|
|
182
186
|
setEmailListener(listener: ((notificationIds: string[]) => void) | null): void;
|
|
187
|
+
/**
|
|
188
|
+
* Abonne `listener` aux demandes qui passent en attente (créées, ou
|
|
189
|
+
* libérées par un membre) : il les reçoit après la validation de la
|
|
190
|
+
* mutation. La fonction rendue désabonne.
|
|
191
|
+
*/
|
|
192
|
+
onPendingRequest(listener: (request: AgentRequest) => void): () => void;
|
|
183
193
|
/**
|
|
184
194
|
* Point unique d'écriture. `fn` reçoit une `Mutation` et s'exécute dans une
|
|
185
195
|
* transaction exclusive ; à sa sortie, les invariants sont vérifiés sur l'état
|
|
@@ -287,6 +297,8 @@ export declare class Mutation {
|
|
|
287
297
|
private readonly touched;
|
|
288
298
|
/** Notifications créées par la mutation dont le destinataire veut un e-mail. */
|
|
289
299
|
readonly emailNotificationIds: string[];
|
|
300
|
+
/** Demandes passées en attente par la mutation : créées, ou libérées. */
|
|
301
|
+
readonly pendingRequestIds: string[];
|
|
290
302
|
/** Demande pour laquelle agit l'agent, lue au début de la mutation. */
|
|
291
303
|
private readonly request;
|
|
292
304
|
/** Jeton avec lequel agit l'agent. */
|