@imenam/mcp-organizer 1.0.1 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +548 -530
- 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 +94 -0
- package/dist/shared/schemas/agent-tools.schema.js +33 -0
- package/dist/src/agent-api/notifications.d.ts +16 -0
- package/dist/src/agent-api/notifications.js +91 -0
- package/dist/src/agent-api/router.js +2 -1
- package/dist/src/cli/watch.d.ts +4 -0
- package/dist/src/cli/watch.js +62 -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 +13 -0
- package/dist/src/mcp/notifications.js +54 -0
- package/dist/src/mcp/start.d.ts +1 -1
- package/dist/src/mcp/start.js +21 -2
- 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
|
@@ -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,83 @@ 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
|
+
/** `GET notifications` : jeton et espace de l'agent, demandes en attente et veilleur du jeton. */
|
|
746
|
+
export declare const NotificationsDataSchema: z.ZodObject<{
|
|
747
|
+
token: z.ZodObject<{
|
|
748
|
+
id: z.ZodString;
|
|
749
|
+
name: z.ZodString;
|
|
750
|
+
}, z.core.$strip>;
|
|
751
|
+
space: z.ZodObject<{
|
|
752
|
+
id: z.ZodString;
|
|
753
|
+
name: z.ZodString;
|
|
754
|
+
kind: z.ZodEnum<{
|
|
755
|
+
personal: "personal";
|
|
756
|
+
shared: "shared";
|
|
757
|
+
}>;
|
|
758
|
+
keyPrefix: z.ZodString;
|
|
759
|
+
}, z.core.$strip>;
|
|
760
|
+
pendingCount: z.ZodNumber;
|
|
761
|
+
watcher: z.ZodObject<{
|
|
762
|
+
armed: z.ZodBoolean;
|
|
763
|
+
since: z.ZodNullable<z.ZodISODateTime>;
|
|
764
|
+
lastSeen: z.ZodNullable<z.ZodISODateTime>;
|
|
765
|
+
}, z.core.$strip>;
|
|
766
|
+
}, z.core.$strip>;
|
|
767
|
+
/**
|
|
768
|
+
* `GET requests/wait` : demandes passées en attente (vide à l'expiration) et
|
|
769
|
+
* curseur à repasser en `since` à l'attente suivante.
|
|
770
|
+
*/
|
|
771
|
+
export declare const WaitRequestsDataSchema: z.ZodObject<{
|
|
772
|
+
space: z.ZodObject<{
|
|
773
|
+
id: z.ZodString;
|
|
774
|
+
name: z.ZodString;
|
|
775
|
+
kind: z.ZodEnum<{
|
|
776
|
+
personal: "personal";
|
|
777
|
+
shared: "shared";
|
|
778
|
+
}>;
|
|
779
|
+
keyPrefix: z.ZodString;
|
|
780
|
+
}, z.core.$strip>;
|
|
781
|
+
requests: z.ZodArray<z.ZodObject<{
|
|
782
|
+
id: z.ZodString;
|
|
783
|
+
kind: z.ZodEnum<{
|
|
784
|
+
idea: "idea";
|
|
785
|
+
idea_promotion: "idea_promotion";
|
|
786
|
+
message: "message";
|
|
787
|
+
remark: "remark";
|
|
788
|
+
test_review: "test_review";
|
|
789
|
+
}>;
|
|
790
|
+
status: z.ZodEnum<{
|
|
791
|
+
cancelled: "cancelled";
|
|
792
|
+
done: "done";
|
|
793
|
+
failed: "failed";
|
|
794
|
+
pending: "pending";
|
|
795
|
+
processing: "processing";
|
|
796
|
+
}>;
|
|
797
|
+
text: z.ZodString;
|
|
798
|
+
author: z.ZodObject<{
|
|
799
|
+
id: z.ZodString;
|
|
800
|
+
name: z.ZodString;
|
|
801
|
+
email: z.ZodString;
|
|
802
|
+
}, z.core.$strip>;
|
|
803
|
+
contextTask: z.ZodNullable<z.ZodObject<{
|
|
804
|
+
id: z.ZodString;
|
|
805
|
+
key: z.ZodString;
|
|
806
|
+
title: z.ZodString;
|
|
807
|
+
}, z.core.$strip>>;
|
|
808
|
+
contextIdeaId: z.ZodNullable<z.ZodString>;
|
|
809
|
+
createdAt: z.ZodISODateTime;
|
|
810
|
+
claimedAt: z.ZodNullable<z.ZodISODateTime>;
|
|
811
|
+
claimedByThisToken: z.ZodBoolean;
|
|
812
|
+
reply: z.ZodNullable<z.ZodString>;
|
|
813
|
+
failureReason: z.ZodNullable<z.ZodString>;
|
|
814
|
+
}, z.core.$strip>>;
|
|
815
|
+
cursor: z.ZodISODateTime;
|
|
816
|
+
timedOut: z.ZodBoolean;
|
|
817
|
+
}, z.core.$strip>;
|
|
818
|
+
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,22 @@ 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
|
+
/** `GET notifications` : jeton et espace de l'agent, demandes en attente et veilleur du jeton. */
|
|
183
|
+
export const NotificationsDataSchema = z.object({
|
|
184
|
+
token: z.object({ id: IdSchema, name: z.string() }),
|
|
185
|
+
space: AgentSpaceSchema,
|
|
186
|
+
pendingCount: z.number().int().min(0),
|
|
187
|
+
watcher: AgentWatcherSchema,
|
|
188
|
+
});
|
|
189
|
+
/**
|
|
190
|
+
* `GET requests/wait` : demandes passées en attente (vide à l'expiration) et
|
|
191
|
+
* curseur à repasser en `since` à l'attente suivante.
|
|
192
|
+
*/
|
|
193
|
+
export const WaitRequestsDataSchema = z.object({
|
|
194
|
+
space: AgentSpaceSchema,
|
|
195
|
+
requests: z.array(AgentRequestSummarySchema),
|
|
196
|
+
cursor: TimestampSchema,
|
|
197
|
+
timedOut: z.boolean(),
|
|
198
|
+
});
|
|
@@ -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,91 @@
|
|
|
1
|
+
import { MAX_REQUEST_LIST_LIMIT, NoQuerySchema, NotificationsDataSchema, WaitRequestsDataSchema, WaitRequestsQuerySchema, } from "../../shared/schemas/agent-tools.schema.js";
|
|
2
|
+
import { resolveAgentToken } from "../core/agent-tokens.js";
|
|
3
|
+
import { requestData, requestLine, spaceData } from "../core/agent.js";
|
|
4
|
+
import { parseInput, respond } from "./types.js";
|
|
5
|
+
/**
|
|
6
|
+
* Notifications de l'agent : état (`GET notifications`) et attente longue
|
|
7
|
+
* des demandes qui passent en attente (`GET requests/wait`), appelée en
|
|
8
|
+
* boucle par le veilleur que lance `enable_notifications`.
|
|
9
|
+
*/
|
|
10
|
+
export const notificationRoutes = [
|
|
11
|
+
{
|
|
12
|
+
method: "GET",
|
|
13
|
+
path: "/notifications",
|
|
14
|
+
handle: ({ store }, { agent, query }) => {
|
|
15
|
+
parseInput(NoQuerySchema, query);
|
|
16
|
+
const watcher = store.watchers.status(agent.token.id);
|
|
17
|
+
const pendingCount = store.countPendingRequestsOfSpace(agent.space.id);
|
|
18
|
+
const state = watcher.armed ? `a watcher is armed for this token since ${watcher.since}` : "no watcher is armed for this token";
|
|
19
|
+
return respond(NotificationsDataSchema, `Space "${agent.space.name}": ${pendingCount} pending request(s); ${state}.`, {
|
|
20
|
+
token: { id: agent.token.id, name: agent.token.name },
|
|
21
|
+
space: spaceData(agent.space),
|
|
22
|
+
pendingCount,
|
|
23
|
+
watcher,
|
|
24
|
+
});
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
/** Demandes en attente de l'espace du jeton modifiées strictement après `since`. */
|
|
29
|
+
function pendingSince(ctx, agent, since) {
|
|
30
|
+
return ctx.store.listAgentRequestsTargeting(agent.space.id, "pending", MAX_REQUEST_LIST_LIMIT).filter((request) => request.updatedAt > since);
|
|
31
|
+
}
|
|
32
|
+
function waitResponse(ctx, agent, requests, cursor, timeoutSeconds) {
|
|
33
|
+
const data = {
|
|
34
|
+
space: spaceData(agent.space),
|
|
35
|
+
requests: requests.map((request) => requestData(ctx.store, agent, request)),
|
|
36
|
+
cursor,
|
|
37
|
+
timedOut: requests.length === 0,
|
|
38
|
+
};
|
|
39
|
+
if (requests.length === 0)
|
|
40
|
+
return respond(WaitRequestsDataSchema, `No new pending request in "${agent.space.name}" after ${timeoutSeconds}s.`, data);
|
|
41
|
+
const lines = requests.map((request) => requestLine(ctx.store, agent, request)).join("\n");
|
|
42
|
+
return respond(WaitRequestsDataSchema, `${requests.length} new pending request(s) in "${agent.space.name}":\n${lines}\n\nNext: call claim_request with the id of the oldest request.`, data);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* `GET requests/wait` : rend aussitôt les demandes passées en attente depuis
|
|
46
|
+
* `since`, sinon attend qu'une demande de l'espace du jeton passe en attente
|
|
47
|
+
* (création ou libération), au plus `timeout_seconds`. Le curseur rendu est
|
|
48
|
+
* l'horodatage de la dernière demande rendue, ou le point de départ de
|
|
49
|
+
* l'attente à l'expiration. Une attente `watcher=true` compte dans la
|
|
50
|
+
* présence du veilleur du jeton.
|
|
51
|
+
*/
|
|
52
|
+
export function waitForPendingRequests(ctx, request, signal) {
|
|
53
|
+
const agent = resolveAgentToken(ctx.store, request.bearer, ctx.now?.() ?? new Date());
|
|
54
|
+
const input = parseInput(WaitRequestsQuerySchema, request.query);
|
|
55
|
+
const startedAt = new Date().toISOString();
|
|
56
|
+
const since = input.since ?? null;
|
|
57
|
+
if (since !== null) {
|
|
58
|
+
const ready = pendingSince(ctx, agent, since);
|
|
59
|
+
if (ready.length > 0)
|
|
60
|
+
return Promise.resolve(waitResponse(ctx, agent, ready, ready.map((r) => r.updatedAt).sort().at(-1), input.timeout_seconds));
|
|
61
|
+
}
|
|
62
|
+
return new Promise((resolve) => {
|
|
63
|
+
const releaseWatcher = input.watcher ? ctx.store.watchers.begin(agent.token.id) : () => { };
|
|
64
|
+
const received = [];
|
|
65
|
+
let flush = null;
|
|
66
|
+
const stop = () => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
if (flush)
|
|
69
|
+
clearImmediate(flush);
|
|
70
|
+
unsubscribe();
|
|
71
|
+
signal.removeEventListener("abort", onAbort);
|
|
72
|
+
releaseWatcher();
|
|
73
|
+
};
|
|
74
|
+
const finish = (requests, cursor) => {
|
|
75
|
+
stop();
|
|
76
|
+
resolve(waitResponse(ctx, agent, requests, cursor, input.timeout_seconds));
|
|
77
|
+
};
|
|
78
|
+
// Les demandes d'une même mutation arrivent ensemble : elles partent dans la même réponse.
|
|
79
|
+
const unsubscribe = ctx.store.onPendingRequest((pending) => {
|
|
80
|
+
if (pending.targetSpaceId !== agent.space.id || pending.status !== "pending")
|
|
81
|
+
return;
|
|
82
|
+
received.push(pending);
|
|
83
|
+
flush ??= setImmediate(() => finish(received, received.map((r) => r.updatedAt).sort().at(-1)));
|
|
84
|
+
});
|
|
85
|
+
const timer = setTimeout(() => finish([], since ?? startedAt), input.timeout_seconds * 1000);
|
|
86
|
+
const onAbort = () => stop();
|
|
87
|
+
signal.addEventListener("abort", onAbort);
|
|
88
|
+
if (signal.aborted)
|
|
89
|
+
stop();
|
|
90
|
+
});
|
|
91
|
+
}
|
|
@@ -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,4 @@
|
|
|
1
|
+
import { type AgentRequestSummary } from "../../shared/schemas/agent-tools.schema.js";
|
|
2
|
+
/** Ligne d'une demande : une seule ligne, lisible telle quelle dans la notification. */
|
|
3
|
+
export declare function formatRequestEvent(request: AgentRequestSummary): string;
|
|
4
|
+
export declare function runWatchMode(tokenId: string | undefined): Promise<number>;
|
|
@@ -0,0 +1,62 @@
|
|
|
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 { readHandoff } from "../mcp/watch-handoff.js";
|
|
5
|
+
/**
|
|
6
|
+
* Mode veilleur (`--watch <identifiant du jeton>`).
|
|
7
|
+
*
|
|
8
|
+
* Une ligne sur stdout = une demande passée en attente. Ce flux est consommé
|
|
9
|
+
* par l'outil `Monitor` de l'hôte, qui réveille la session de l'agent à chaque
|
|
10
|
+
* ligne, y compris quand elle est inactive. Le journal part sur stderr.
|
|
11
|
+
*
|
|
12
|
+
* Une panne réseau ou du site est réessayée avec un délai croissant. Un refus
|
|
13
|
+
* du site (jeton révoqué, expiré, route inconnue) arrête le veilleur après une
|
|
14
|
+
* dernière ligne qui en donne la raison.
|
|
15
|
+
*/
|
|
16
|
+
const BACKOFF_MIN_MS = 1_000;
|
|
17
|
+
const BACKOFF_MAX_MS = 30_000;
|
|
18
|
+
/** Longueur maximale de l'extrait du texte de la demande. */
|
|
19
|
+
const EXCERPT_LENGTH = 120;
|
|
20
|
+
/** Ligne d'une demande : une seule ligne, lisible telle quelle dans la notification. */
|
|
21
|
+
export function formatRequestEvent(request) {
|
|
22
|
+
const text = request.text.replace(/\s+/g, " ").trim();
|
|
23
|
+
const excerpt = text.length > EXCERPT_LENGTH ? `${text.slice(0, EXCERPT_LENGTH - 1)}…` : text;
|
|
24
|
+
const context = request.contextTask ? ` · ${request.contextTask.key}` : "";
|
|
25
|
+
return `[organizer] ${request.kind} · ${request.author.name} — "${excerpt}"${context} (${request.id})`;
|
|
26
|
+
}
|
|
27
|
+
/** Refus définitif du site : un nouvel essai donnerait la même réponse. */
|
|
28
|
+
function isRefusal(error) {
|
|
29
|
+
return error instanceof SiteError && error.status !== undefined && error.status >= 400 && error.status < 500 && error.status !== 408 && error.status !== 429;
|
|
30
|
+
}
|
|
31
|
+
export async function runWatchMode(tokenId) {
|
|
32
|
+
if (!tokenId || tokenId.startsWith("--")) {
|
|
33
|
+
throw new ConfigError("--watch needs the token id given by the enable_notifications tool: `mcp-organizer --watch <token id>`.");
|
|
34
|
+
}
|
|
35
|
+
const handoff = readHandoff(tokenId);
|
|
36
|
+
if (!handoff) {
|
|
37
|
+
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.`);
|
|
38
|
+
}
|
|
39
|
+
const client = new OrganizerSiteClient({ url: handoff.url, token: handoff.token });
|
|
40
|
+
process.stderr.write(`watching token ${tokenId} on ${handoff.url}\n`);
|
|
41
|
+
let since;
|
|
42
|
+
let backoff = BACKOFF_MIN_MS;
|
|
43
|
+
for (;;) {
|
|
44
|
+
try {
|
|
45
|
+
const result = await client.waitForRequests({ timeout_seconds: MAX_WAIT_SECONDS, since, watcher: true });
|
|
46
|
+
backoff = BACKOFF_MIN_MS;
|
|
47
|
+
since = result.cursor;
|
|
48
|
+
for (const request of result.requests)
|
|
49
|
+
process.stdout.write(`${formatRequestEvent(request)}\n`);
|
|
50
|
+
}
|
|
51
|
+
catch (error) {
|
|
52
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
53
|
+
if (isRefusal(error)) {
|
|
54
|
+
process.stdout.write(`[organizer] notifications stopped: ${detail.replace(/\s+/g, " ")}\n`);
|
|
55
|
+
return 1;
|
|
56
|
+
}
|
|
57
|
+
process.stderr.write(`wait failed, retrying in ${Math.round(backoff / 1000)}s: ${detail}\n`);
|
|
58
|
+
await new Promise((resolve) => setTimeout(resolve, backoff));
|
|
59
|
+
backoff = Math.min(backoff * 2, BACKOFF_MAX_MS);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
@@ -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. */
|
package/dist/src/core/store.js
CHANGED
|
@@ -2,6 +2,7 @@ import { openDatabase } from "./db.js";
|
|
|
2
2
|
import { ForbiddenError, InvariantError, NotFoundError, OrganizerError } from "./errors.js";
|
|
3
3
|
import { positionBetween } from "../../shared/logic/positions.js";
|
|
4
4
|
import { newId } from "./ids.js";
|
|
5
|
+
import { WatcherRegistry } from "./watchers.js";
|
|
5
6
|
import { MAX_TASK_DEPTH, } from "../../shared/schemas/enums.schema.js";
|
|
6
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
8
|
/** Valide `value` avec `schema` ; une donnée invalide est une erreur métier qui liste les champs fautifs. */
|
|
@@ -276,6 +277,9 @@ export class Store {
|
|
|
276
277
|
db;
|
|
277
278
|
activeMutation = null;
|
|
278
279
|
emailListener = null;
|
|
280
|
+
pendingRequestListeners = new Set();
|
|
281
|
+
/** Présence des veilleurs de notifications, par jeton d'agent. */
|
|
282
|
+
watchers = new WatcherRegistry();
|
|
279
283
|
constructor(db) {
|
|
280
284
|
this.db = db;
|
|
281
285
|
}
|
|
@@ -767,6 +771,15 @@ export class Store {
|
|
|
767
771
|
setEmailListener(listener) {
|
|
768
772
|
this.emailListener = listener;
|
|
769
773
|
}
|
|
774
|
+
/**
|
|
775
|
+
* Abonne `listener` aux demandes qui passent en attente (créées, ou
|
|
776
|
+
* libérées par un membre) : il les reçoit après la validation de la
|
|
777
|
+
* mutation. La fonction rendue désabonne.
|
|
778
|
+
*/
|
|
779
|
+
onPendingRequest(listener) {
|
|
780
|
+
this.pendingRequestListeners.add(listener);
|
|
781
|
+
return () => this.pendingRequestListeners.delete(listener);
|
|
782
|
+
}
|
|
770
783
|
/**
|
|
771
784
|
* Point unique d'écriture. `fn` reçoit une `Mutation` et s'exécute dans une
|
|
772
785
|
* transaction exclusive ; à sa sortie, les invariants sont vérifiés sur l'état
|
|
@@ -777,6 +790,7 @@ export class Store {
|
|
|
777
790
|
if (this.activeMutation)
|
|
778
791
|
throw new Error("Store.mutate is not reentrant: use the Mutation passed to the running mutation.");
|
|
779
792
|
let emails = [];
|
|
793
|
+
let pending = [];
|
|
780
794
|
const run = this.db.transaction(() => {
|
|
781
795
|
const tx = new Mutation(this, actor);
|
|
782
796
|
this.activeMutation = tx;
|
|
@@ -784,6 +798,7 @@ export class Store {
|
|
|
784
798
|
const result = fn(tx);
|
|
785
799
|
tx.finish();
|
|
786
800
|
emails = tx.emailNotificationIds;
|
|
801
|
+
pending = tx.pendingRequestIds;
|
|
787
802
|
return result;
|
|
788
803
|
}
|
|
789
804
|
finally {
|
|
@@ -793,6 +808,13 @@ export class Store {
|
|
|
793
808
|
const result = run.immediate();
|
|
794
809
|
if (emails.length > 0 && this.emailListener)
|
|
795
810
|
this.emailListener(emails);
|
|
811
|
+
if (pending.length > 0 && this.pendingRequestListeners.size > 0) {
|
|
812
|
+
for (const id of pending) {
|
|
813
|
+
const request = this.getAgentRequest(id);
|
|
814
|
+
for (const listener of [...this.pendingRequestListeners])
|
|
815
|
+
listener(request);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
796
818
|
return result;
|
|
797
819
|
}
|
|
798
820
|
}
|
|
@@ -809,6 +831,8 @@ export class Mutation {
|
|
|
809
831
|
touched = new Map();
|
|
810
832
|
/** Notifications créées par la mutation dont le destinataire veut un e-mail. */
|
|
811
833
|
emailNotificationIds = [];
|
|
834
|
+
/** Demandes passées en attente par la mutation : créées, ou libérées. */
|
|
835
|
+
pendingRequestIds = [];
|
|
812
836
|
/** Demande pour laquelle agit l'agent, lue au début de la mutation. */
|
|
813
837
|
request;
|
|
814
838
|
/** Jeton avec lequel agit l'agent. */
|
|
@@ -1690,6 +1714,7 @@ export class Mutation {
|
|
|
1690
1714
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
1691
1715
|
.run(request.id, request.authorId, request.targetSpaceId, request.kind, request.contextTaskId, request.contextIdeaId, request.text, request.status, request.createdAt, request.updatedAt);
|
|
1692
1716
|
this.createdRequestIds.add(request.id);
|
|
1717
|
+
this.pendingRequestIds.push(request.id);
|
|
1693
1718
|
this.touchRequest(request);
|
|
1694
1719
|
return request;
|
|
1695
1720
|
}
|
|
@@ -1735,6 +1760,7 @@ export class Mutation {
|
|
|
1735
1760
|
throw new InvariantError(`Request ${id} is ${current.status}: only a processing request can be released.`);
|
|
1736
1761
|
this.db.prepare("UPDATE agent_requests SET status = 'pending', claimed_at = NULL, claimed_by_token_id = NULL, updated_at = ? WHERE id = ?").run(this.now, id);
|
|
1737
1762
|
const released = this.store.getAgentRequest(id);
|
|
1763
|
+
this.pendingRequestIds.push(id);
|
|
1738
1764
|
this.touchRequest(released);
|
|
1739
1765
|
return released;
|
|
1740
1766
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Présence des veilleurs de notifications, par jeton d'agent.
|
|
3
|
+
*
|
|
4
|
+
* Un veilleur enchaîne des attentes longues sur `GET /api/agent/requests/wait`.
|
|
5
|
+
* Le registre rend sa présence observable : `enable_notifications` répond
|
|
6
|
+
* « déjà armé », la page Agent de la GUI affiche « à l'écoute ».
|
|
7
|
+
*
|
|
8
|
+
* État de connexion, donc volatile : il vit en mémoire du processus du site et
|
|
9
|
+
* disparaît avec lui.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Délai de grâce entre deux attentes d'un même veilleur : le jeton reste
|
|
13
|
+
* « à l'écoute » pendant le court intervalle où aucune attente n'est ouverte.
|
|
14
|
+
*/
|
|
15
|
+
export declare const WATCHER_GRACE_MS: number;
|
|
16
|
+
export interface WatcherStatus {
|
|
17
|
+
armed: boolean;
|
|
18
|
+
/** Début de la série d'attentes en cours, null sans veilleur. */
|
|
19
|
+
since: string | null;
|
|
20
|
+
/** Dernière activité connue d'un veilleur de ce jeton, null si aucune. */
|
|
21
|
+
lastSeen: string | null;
|
|
22
|
+
}
|
|
23
|
+
export declare class WatcherRegistry {
|
|
24
|
+
private readonly now;
|
|
25
|
+
private readonly presence;
|
|
26
|
+
constructor(now?: () => number);
|
|
27
|
+
/** Déclare une attente de veilleur du jeton `tokenId`. La fonction rendue la clôt ; elle doit toujours être appelée. */
|
|
28
|
+
begin(tokenId: string): () => void;
|
|
29
|
+
status(tokenId: string): WatcherStatus;
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Présence des veilleurs de notifications, par jeton d'agent.
|
|
3
|
+
*
|
|
4
|
+
* Un veilleur enchaîne des attentes longues sur `GET /api/agent/requests/wait`.
|
|
5
|
+
* Le registre rend sa présence observable : `enable_notifications` répond
|
|
6
|
+
* « déjà armé », la page Agent de la GUI affiche « à l'écoute ».
|
|
7
|
+
*
|
|
8
|
+
* État de connexion, donc volatile : il vit en mémoire du processus du site et
|
|
9
|
+
* disparaît avec lui.
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* Délai de grâce entre deux attentes d'un même veilleur : le jeton reste
|
|
13
|
+
* « à l'écoute » pendant le court intervalle où aucune attente n'est ouverte.
|
|
14
|
+
*/
|
|
15
|
+
export const WATCHER_GRACE_MS = 2 * 60 * 1000;
|
|
16
|
+
export class WatcherRegistry {
|
|
17
|
+
now;
|
|
18
|
+
presence = new Map();
|
|
19
|
+
constructor(now = Date.now) {
|
|
20
|
+
this.now = now;
|
|
21
|
+
}
|
|
22
|
+
/** Déclare une attente de veilleur du jeton `tokenId`. La fonction rendue la clôt ; elle doit toujours être appelée. */
|
|
23
|
+
begin(tokenId) {
|
|
24
|
+
const now = this.now();
|
|
25
|
+
const current = this.presence.get(tokenId);
|
|
26
|
+
const continuing = current !== undefined && (current.inflight > 0 || now - current.lastSeen <= WATCHER_GRACE_MS);
|
|
27
|
+
this.presence.set(tokenId, continuing ? { ...current, inflight: current.inflight + 1, lastSeen: now } : { inflight: 1, since: now, lastSeen: now });
|
|
28
|
+
let released = false;
|
|
29
|
+
return () => {
|
|
30
|
+
if (released)
|
|
31
|
+
return;
|
|
32
|
+
released = true;
|
|
33
|
+
const active = this.presence.get(tokenId);
|
|
34
|
+
if (!active)
|
|
35
|
+
return;
|
|
36
|
+
this.presence.set(tokenId, { ...active, inflight: Math.max(0, active.inflight - 1), lastSeen: this.now() });
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
status(tokenId) {
|
|
40
|
+
const entry = this.presence.get(tokenId);
|
|
41
|
+
if (!entry)
|
|
42
|
+
return { armed: false, since: null, lastSeen: null };
|
|
43
|
+
const armed = entry.inflight > 0 || this.now() - entry.lastSeen <= WATCHER_GRACE_MS;
|
|
44
|
+
if (!armed) {
|
|
45
|
+
this.presence.delete(tokenId);
|
|
46
|
+
return { armed: false, since: null, lastSeen: new Date(entry.lastSeen).toISOString() };
|
|
47
|
+
}
|
|
48
|
+
return { armed: true, since: new Date(entry.since).toISOString(), lastSeen: new Date(entry.lastSeen).toISOString() };
|
|
49
|
+
}
|
|
50
|
+
}
|
package/dist/src/index.js
CHANGED
|
@@ -19,6 +19,10 @@ Usage :
|
|
|
19
19
|
(dossier <fichier>.attachments).
|
|
20
20
|
mcp-organizer --claude-setup-mcp [--force] --url=<URL du site> --token=<jeton>
|
|
21
21
|
Écrit l'entrée du mode MCP dans ./.mcp.json.
|
|
22
|
+
mcp-organizer --watch <id du jeton>
|
|
23
|
+
Veilleur de notifications : une ligne par demande passée en attente.
|
|
24
|
+
Lancé par l'outil Monitor de l'agent, avec la commande que rend
|
|
25
|
+
l'outil enable_notifications.
|
|
22
26
|
mcp-organizer --version Affiche la version.
|
|
23
27
|
mcp-organizer --help Affiche cette aide.
|
|
24
28
|
|
|
@@ -50,6 +54,12 @@ async function main() {
|
|
|
50
54
|
const { runSetupMcp } = await import("./cli/setup-mcp.js");
|
|
51
55
|
process.exit(runSetupMcp(argv, process.cwd()));
|
|
52
56
|
}
|
|
57
|
+
// Le veilleur lit l'URL du site et le jeton dans le relais déposé par enable_notifications.
|
|
58
|
+
const watchIndex = argv.indexOf("--watch");
|
|
59
|
+
if (watchIndex !== -1) {
|
|
60
|
+
const { runWatchMode } = await import("./cli/watch.js");
|
|
61
|
+
process.exit(await runWatchMode(argv[watchIndex + 1]));
|
|
62
|
+
}
|
|
53
63
|
const rootDir = findProjectRoot(path.dirname(fileURLToPath(import.meta.url)));
|
|
54
64
|
const siteCommand = SITE_COMMANDS.some((flag) => argv.includes(flag));
|
|
55
65
|
// Les commandes du site ouvrent la base : better-sqlite3 doit avoir son module natif.
|