kather-relay 0.1.0 → 0.2.0

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 CHANGED
@@ -5,11 +5,13 @@ Le Relais de [Kather](https://kather.katalyx.fr) : il fait apparaître les Agent
5
5
  Node 22.12 ou plus.
6
6
 
7
7
  ```sh
8
- npx kather-relay connect https://kather.katalyx.fr # code à confirmer dans Kather, puis consentement T3 Code (Full access)
9
- npx kather-relay install-service # service utilisateur (systemd --user, launchd)
10
- npx kather-relay status # appairage, T3 Code, service
8
+ npx kather-relay@latest connect https://kather.katalyx.fr # code à confirmer dans Kather, puis consentement T3 Code (Full access)
9
+ npx kather-relay@latest install-service # service utilisateur (systemd --user, launchd)
10
+ npx kather-relay@latest status # appairage, T3 Code, service
11
11
  ```
12
12
 
13
13
  Le plus simple : dans Kather, copie le prompt d'installation du Relais et colle-le dans ton T3 Code.
14
14
 
15
+ Kather refuse un Relais trop ancien (bandeau « Mets à jour ton Relais ») : relance `npx kather-relay@latest install-service`, qui remplace la copie du service.
16
+
15
17
  Autres commandes : `kather-relay login` (reconnecter T3 Code), `kather-relay run` (au premier plan), `kather-relay uninstall-service`. Option `--redirect-host localhost` si T3 Code refuse la redirection vers `127.0.0.1`.
@@ -3776,6 +3776,22 @@ import { hostname as hostname3 } from "node:os";
3776
3776
  import { dirname, join as join3, resolve } from "node:path";
3777
3777
  import { pathToFileURL } from "node:url";
3778
3778
 
3779
+ // ../../packages/shared/src/geometry.ts
3780
+ function rectangle(minX, minZ, maxX, maxZ) {
3781
+ return [
3782
+ { x: minX, z: minZ },
3783
+ { x: maxX, z: minZ },
3784
+ { x: maxX, z: maxZ },
3785
+ { x: minX, z: maxZ }
3786
+ ];
3787
+ }
3788
+
3789
+ // ../../packages/shared/src/ranges.ts
3790
+ var NAME_TAG_RANGE = 6;
3791
+
3792
+ // ../../packages/shared/src/agent-scene.ts
3793
+ var AGENT_PILL_RANGE = NAME_TAG_RANGE * 2;
3794
+
3779
3795
  // ../../node_modules/.pnpm/zod@4.6.7/node_modules/zod/v4/classic/external.js
3780
3796
  var external_exports = {};
3781
3797
  __export(external_exports, {
@@ -23512,6 +23528,24 @@ function katherStatusOf(t3Status, pendingRequestCount = 0) {
23512
23528
  if (pendingRequestCount > 0) return "waiting";
23513
23529
  return BY_T3_STATUS[t3Status] ?? "available";
23514
23530
  }
23531
+ var short = external_exports.string().max(2e3);
23532
+ var pendingRequestSchema = external_exports.object({
23533
+ requestId: external_exports.string().min(1).max(512),
23534
+ questions: external_exports.array(
23535
+ external_exports.object({
23536
+ id: external_exports.string().min(1).max(512),
23537
+ header: short.nullable(),
23538
+ question: short,
23539
+ options: external_exports.array(external_exports.object({ label: short, value: short.optional(), description: short.optional() })).max(20),
23540
+ multiSelect: external_exports.boolean()
23541
+ })
23542
+ ).max(10)
23543
+ });
23544
+ var agentRequestSchema = external_exports.discriminatedUnion("kind", [
23545
+ external_exports.object({ kind: external_exports.literal("question"), request: pendingRequestSchema }),
23546
+ external_exports.object({ kind: external_exports.literal("approval") })
23547
+ ]);
23548
+ var agentPlaceSchema = external_exports.enum(["desk", "raised", "coming", "near", "returning"]);
23515
23549
  var agentViewSchema = external_exports.object({
23516
23550
  /** Identité stable : `environmentId` T3 + `threadId`. */
23517
23551
  id: external_exports.string(),
@@ -23524,11 +23558,19 @@ var agentViewSchema = external_exports.object({
23524
23558
  title: external_exports.string(),
23525
23559
  project: external_exports.string().nullable(),
23526
23560
  status: agentStatusSchema,
23527
- pendingRequest: external_exports.boolean(),
23561
+ /**
23562
+ * Ce que l'Agent attend, en Attend ; `null` sinon, et pour qui ne peut pas lire le fil (un
23563
+ * Invité, un autre Membre face à un Agent privé).
23564
+ */
23565
+ request: agentRequestSchema.nullable(),
23566
+ /** La marque À relire (glossaire), tenue par le serveur : visible de tous, Invités compris. */
23567
+ toReview: external_exports.boolean(),
23528
23568
  subagents: external_exports.object({ active: external_exports.number().int(), total: external_exports.number().int() }),
23529
23569
  /** La rangée du Propriétaire et le Poste dans la rangée ; `null` : « sans Poste ». */
23530
23570
  rowIndex: external_exports.number().int().nullable(),
23531
23571
  deskIndex: external_exports.number().int().nullable(),
23572
+ /** Où il en est dans la scène : à son Poste, en route, près de son Propriétaire… */
23573
+ place: agentPlaceSchema,
23532
23574
  /** Hors ligne depuis cet instant (ms depuis l'époque), ou `null` si le Relais répond. */
23533
23575
  offlineSince: external_exports.number().nullable(),
23534
23576
  /** Réglé par le Propriétaire (US 71) : les autres ne voient que Statut, titre et Propriétaire. */
@@ -23542,6 +23584,12 @@ var agentRowOwnerSchema = external_exports.object({
23542
23584
  name: external_exports.string(),
23543
23585
  outfit: external_exports.string()
23544
23586
  });
23587
+ var agentPoseSchema = external_exports.object({
23588
+ id: external_exports.string(),
23589
+ x: external_exports.number(),
23590
+ z: external_exports.number(),
23591
+ heading: external_exports.number()
23592
+ });
23545
23593
  var agentsMessageSchema = external_exports.discriminatedUnion("event", [
23546
23594
  external_exports.object({
23547
23595
  type: external_exports.literal("agents"),
@@ -23559,19 +23607,6 @@ var agentsMessageSchema = external_exports.discriminatedUnion("event", [
23559
23607
  })
23560
23608
  ]);
23561
23609
  var agentAlertSchema = external_exports.enum(["waiting", "done"]);
23562
- var short = external_exports.string().max(2e3);
23563
- var pendingRequestSchema = external_exports.object({
23564
- requestId: external_exports.string().min(1).max(512),
23565
- questions: external_exports.array(
23566
- external_exports.object({
23567
- id: external_exports.string().min(1).max(512),
23568
- header: short.nullable(),
23569
- question: short,
23570
- options: external_exports.array(external_exports.object({ label: short, value: short.optional(), description: short.optional() })).max(20),
23571
- multiSelect: external_exports.boolean()
23572
- })
23573
- ).max(10)
23574
- });
23575
23610
  var looseRequestSchema = external_exports.looseObject({
23576
23611
  requestId: external_exports.string().min(1).max(512),
23577
23612
  questions: external_exports.array(
@@ -23590,15 +23625,23 @@ var looseRequestSchema = external_exports.looseObject({
23590
23625
  })
23591
23626
  ).max(10)
23592
23627
  });
23593
-
23594
- // ../../packages/shared/src/geometry.ts
23595
- function rectangle(minX, minZ, maxX, maxZ) {
23596
- return [
23597
- { x: minX, z: minZ },
23598
- { x: maxX, z: minZ },
23599
- { x: maxX, z: maxZ },
23600
- { x: minX, z: maxZ }
23601
- ];
23628
+ function pendingRequestsFrom(result) {
23629
+ const parsed = external_exports.looseObject({ requests: external_exports.array(looseRequestSchema).max(20) }).safeParse(result);
23630
+ if (!parsed.success) return null;
23631
+ return parsed.data.requests.map((r) => ({
23632
+ requestId: r.requestId,
23633
+ questions: r.questions.map((q) => ({
23634
+ id: q.id,
23635
+ header: q.header ?? null,
23636
+ question: q.question ?? "",
23637
+ options: (q.options ?? []).map((o) => ({
23638
+ label: o.label,
23639
+ ...o.value != null ? { value: String(o.value) } : {},
23640
+ ...o.description ? { description: o.description } : {}
23641
+ })),
23642
+ multiSelect: q.multiSelect ?? false
23643
+ }))
23644
+ }));
23602
23645
  }
23603
23646
 
23604
23647
  // ../../packages/shared/src/appearance.ts
@@ -23815,7 +23858,7 @@ var parisParts = new Intl.DateTimeFormat("en-GB", {
23815
23858
  });
23816
23859
 
23817
23860
  // ../../packages/shared/src/relay-protocol.ts
23818
- var RELAY_PROTOCOL_VERSION = 2;
23861
+ var RELAY_PROTOCOL_VERSION = 3;
23819
23862
  var RELAY_HEARTBEAT_MS = 3e4;
23820
23863
  var id = external_exports.string().min(1).max(512);
23821
23864
  var name = external_exports.string().max(500);
@@ -23829,8 +23872,12 @@ var relayAgentSchema = external_exports.object({
23829
23872
  projectId: id.nullable(),
23830
23873
  projectName: name.nullable(),
23831
23874
  status: agentStatusSchema,
23832
- /** Une demande en attente (`t3_pending_request`), lue pour les Agents en Attend. */
23833
- pendingRequest: external_exports.boolean(),
23875
+ /**
23876
+ * Le résumé de ce que l'Agent attend, en Attend : la première demande listée par
23877
+ * `t3_pending_request` (question et options), ou `approval` quand T3 Code compte une demande
23878
+ * sans en lister aucune. `null` hors Attend, ou tant que la demande n'a pas pu être lue.
23879
+ */
23880
+ request: agentRequestSchema.nullable(),
23834
23881
  subagents: subagentCountSchema,
23835
23882
  /** Le lien du thread dans T3 Code (`t3-thread://…`), pour l'y ouvrir. */
23836
23883
  link: external_exports.string().max(2e3).nullable()
@@ -23880,6 +23927,11 @@ var relayMessageSchema = external_exports.discriminatedUnion("type", [
23880
23927
  hasMore: external_exports.boolean()
23881
23928
  }),
23882
23929
  external_exports.object({ type: external_exports.literal("t3.auth"), ...t3AuthSchema.shape }),
23930
+ /**
23931
+ * Un Agent a écrit à un autre Agent de la même machine : un `user_message` de `toThreadId`
23932
+ * porte `createdBy: "agent"` et `senderThreadId: fromThreadId`. Jamais pour un sous-agent.
23933
+ */
23934
+ external_exports.object({ type: external_exports.literal("agents.exchange"), fromThreadId: id, toThreadId: id }),
23883
23935
  external_exports.discriminatedUnion("ok", [
23884
23936
  external_exports.object({
23885
23937
  type: external_exports.literal("command.result"),
@@ -24037,13 +24089,19 @@ var clientMessageSchema = external_exports.discriminatedUnion("type", [
24037
24089
  external_exports.union([external_exports.string().max(2e4), external_exports.array(external_exports.string().max(2e3)).max(20)])
24038
24090
  )
24039
24091
  }),
24040
- external_exports.object({ type: external_exports.literal("agent.setPrivate"), agentId: ref, ref, private: external_exports.boolean() })
24092
+ external_exports.object({ type: external_exports.literal("agent.setPrivate"), agentId: ref, ref, private: external_exports.boolean() }),
24093
+ /**
24094
+ * « Retirer de l'Espace » (US 35) : le Propriétaire retire tout de suite un Agent Hors ligne, au
24095
+ * lieu d'attendre 24 h. S'il revient en ligne, il réapparaît comme un nouvel Agent.
24096
+ */
24097
+ external_exports.object({ type: external_exports.literal("agent.remove"), agentId: ref, ref })
24041
24098
  ]);
24042
24099
  var deliverySchema = external_exports.enum(["started", "queued", "steered"]);
24043
24100
  var agentErrorSchema = external_exports.enum([
24044
24101
  "forbidden",
24045
24102
  "unknown-agent",
24046
24103
  "offline",
24104
+ "online",
24047
24105
  "refused",
24048
24106
  "escalation",
24049
24107
  "failed"
@@ -24075,14 +24133,14 @@ var serverMessageSchema = external_exports.discriminatedUnion("type", [
24075
24133
  occupants: external_exports.array(external_exports.object({ id: external_exports.string(), ...poseSchema.shape, audioKey: audioKeySchema }))
24076
24134
  }),
24077
24135
  /**
24078
- * Ma clé audio et les identités autorisées à s'abonner à mon micro (`null` : tout le monde).
24079
- * Envoyé à l'entrée, puis à chaque changement ; le client le pose par
24080
- * `setTrackSubscriptionPermissions`.
24136
+ * Ma clé audio et les identités autorisées à s'abonner à mon micro, ma caméra et mon écran : les
24137
+ * occupants de même clé, ceux du dehors quand je suis dehors (`listenersAllowed`). Envoyé à
24138
+ * l'entrée, puis à chaque changement ; le client le pose par `setTrackSubscriptionPermissions`.
24081
24139
  */
24082
24140
  external_exports.object({
24083
24141
  type: external_exports.literal("audio"),
24084
24142
  key: audioKeySchema,
24085
- allowed: external_exports.array(external_exports.string()).nullable()
24143
+ allowed: external_exports.array(external_exports.string())
24086
24144
  }),
24087
24145
  /** Création d'un Aparté ou changement de ses participants, pour tout l'Espace. */
24088
24146
  external_exports.object({ type: external_exports.literal("aparte.update"), aparte: aparteSchema }),
@@ -24118,6 +24176,11 @@ var serverMessageSchema = external_exports.discriminatedUnion("type", [
24118
24176
  external_exports.object({ type: external_exports.literal("correction"), ...poseSchema.shape }),
24119
24177
  /** Les Agents de l'Espace : instantané à l'entrée, puis changements. */
24120
24178
  agentsMessageSchema,
24179
+ /**
24180
+ * 15 Hz, à tout l'Espace : la position des Agents hors de leur Poste, calculée par le serveur
24181
+ * (en route, dans une file). Un Agent absent de la liste est à son Poste. Envoyé aussi à l'entrée.
24182
+ */
24183
+ external_exports.object({ type: external_exports.literal("agents.poses"), poses: external_exports.array(agentPoseSchema) }),
24121
24184
  /** Le fil d'un Agent, à qui a le droit de le lire et l'a ouvert : ses derniers éléments. */
24122
24185
  external_exports.object({
24123
24186
  type: external_exports.literal("agent.thread"),
@@ -24146,6 +24209,17 @@ var serverMessageSchema = external_exports.discriminatedUnion("type", [
24146
24209
  error: agentErrorSchema
24147
24210
  })
24148
24211
  ]),
24212
+ /**
24213
+ * Aux Membres de l'Espace (pas aux Invités) : l'Agent `from` vient d'écrire à l'Agent `to`, de
24214
+ * la même machine. Le client trace un trait entre eux pendant ~10 s (`AGENT_LINK_MS`).
24215
+ */
24216
+ external_exports.object({ type: external_exports.literal("agents.exchange"), from: external_exports.string(), to: external_exports.string() }),
24217
+ /**
24218
+ * Au Propriétaire : ses Relais refusés parce que trop anciens (leurs noms), pour le bandeau
24219
+ * « Mets à jour ton Relais ». Envoyé à l'entrée s'il y en a, et à chaque changement ; une liste
24220
+ * vide retire le bandeau.
24221
+ */
24222
+ external_exports.object({ type: external_exports.literal("relays.outdated"), relays: external_exports.array(external_exports.string()) }),
24149
24223
  /** Au Propriétaire seulement : son Agent attend ou a fini (US 66). */
24150
24224
  external_exports.object({
24151
24225
  type: external_exports.literal("notify"),
@@ -24183,9 +24257,6 @@ var TEXTS = {
24183
24257
  tu: "La connexion avec Google a \xE9chou\xE9. R\xE9essaie dans un instant.",
24184
24258
  vous: "La connexion avec Google a \xE9chou\xE9. R\xE9essayez dans un instant."
24185
24259
  },
24186
- "space.choose.title": { tu: "O\xF9 veux-tu entrer ?", vous: "O\xF9 voulez-vous entrer ?" },
24187
- "space.choose.enter": same("Entrer"),
24188
- "space.choose.last": same("Dernier utilis\xE9"),
24189
24260
  "space.none": {
24190
24261
  tu: "Tu n\u2019as acc\xE8s \xE0 aucun Espace. Demande l\u2019acc\xE8s \xE0 un associ\xE9 de Katalyx.",
24191
24262
  vous: "Vous n\u2019avez acc\xE8s \xE0 aucun Espace."
@@ -24193,10 +24264,37 @@ var TEXTS = {
24193
24264
  "space.entering": same("Entr\xE9e dans l\u2019Espace {space}\u2026"),
24194
24265
  "space.title": same("Espace {space}"),
24195
24266
  "space.help": {
24196
- tu: "Clique au sol ou marche avec Z Q S D. Clic droit gliss\xE9 pour tourner la cam\xE9ra, molette pour zoomer.",
24267
+ tu: "Clique au sol ou marche avec Z Q S D. Clic droit gliss\xE9 pour tourner la cam\xE9ra, molette pour zoomer. Clique sur un robot pour ouvrir son Agent.",
24197
24268
  vous: "Cliquez au sol ou marchez avec Z Q S D. Clic droit gliss\xE9 pour tourner la cam\xE9ra, molette pour zoomer."
24198
24269
  },
24199
- "menu.open": same("Menu"),
24270
+ "space.help.dismiss": same("Compris"),
24271
+ // Les Coins de l'Espace (ADR 0008).
24272
+ "corner.space": same("L\u2019Espace"),
24273
+ "corner.space.menu": same("Changer d\u2019Espace ou se d\xE9connecter"),
24274
+ "corner.space.guestMenu": same("Se d\xE9connecter"),
24275
+ "corner.people": same("Pr\xE9sents, Agents et R\xE9glages"),
24276
+ "corner.me": same("Mes commandes"),
24277
+ "corner.view": same("La vue"),
24278
+ "presents.button": same("Pr\xE9sents ({count})"),
24279
+ "agents.button": same("Agents"),
24280
+ "agents.waitingOne": { tu: "1 t\u2019attend", vous: "1 vous attend" },
24281
+ "agents.waitingCount": { tu: "{count} t\u2019attendent", vous: "{count} vous attendent" },
24282
+ "agents.reviewCount": same("{count} \xE0 relire"),
24283
+ "me.place.passage": same("Passage"),
24284
+ "moment.label": same("Action du moment"),
24285
+ "moment.aparte": same("Apart\xE9 avec {names}"),
24286
+ "moment.enter": same("Entr\xE9e"),
24287
+ "drawer.close": same("Fermer (\xC9chap)"),
24288
+ "drawer.back": same("Retour \xE0 la liste"),
24289
+ goTo: same("Aller vers"),
24290
+ // Raccourcis (Réglages > Raccourcis).
24291
+ "shortcuts.walk": same("Marcher"),
24292
+ "shortcuts.mic": same("Couper ou rouvrir le micro"),
24293
+ "shortcuts.videos": same("Basculer entre l\u2019Espace et la Vue Vid\xE9os"),
24294
+ "shortcuts.open": same("Ouvrir l\u2019Agent vis\xE9"),
24295
+ "shortcuts.send": same("Envoyer un message \xE0 l\u2019Agent"),
24296
+ "shortcuts.escape": same("Fermer le tiroir, sortir du plein \xE9cran"),
24297
+ "shortcuts.turn": same("Quart de tour, en vue Iso"),
24200
24298
  "menu.switchSpace": same("Changer d\u2019Espace"),
24201
24299
  "menu.settings": same("R\xE9glages"),
24202
24300
  "menu.invite": same("Inviter"),
@@ -24212,6 +24310,11 @@ var TEXTS = {
24212
24310
  vous: "L\u2019enregistrement a \xE9chou\xE9. R\xE9essayez dans un instant."
24213
24311
  },
24214
24312
  "appearance.legend": { tu: "Ton Apparence", vous: "Votre Apparence" },
24313
+ "appearance.change": same("Changer"),
24314
+ "appearance.drawn": {
24315
+ tu: "Tir\xE9e au hasard. \xAB Changer \xBB en tire une autre.",
24316
+ vous: "Tir\xE9e au hasard. \xAB Changer \xBB en tire une autre."
24317
+ },
24215
24318
  "appearance.skin": same("Peau"),
24216
24319
  "appearance.outfit": same("Tenue"),
24217
24320
  "appearance.hairStyle": same("Coiffure"),
@@ -24264,8 +24367,8 @@ var TEXTS = {
24264
24367
  },
24265
24368
  // Le temps que la restriction du micro s'applique : rien n'est encore promis.
24266
24369
  "zone.bannerPending": {
24267
- tu: "Zone priv\xE9e : {name} \xB7 Micro suspendu le temps de fermer la Zone",
24268
- vous: "Zone priv\xE9e : {name} \xB7 Micro suspendu le temps de fermer la Zone"
24370
+ tu: "Zone priv\xE9e : {name} \xB7 Micro et cam\xE9ra suspendus le temps de fermer la Zone",
24371
+ vous: "Zone priv\xE9e : {name} \xB7 Micro et cam\xE9ra suspendus le temps de fermer la Zone"
24269
24372
  },
24270
24373
  "aparte.name": same("Apart\xE9"),
24271
24374
  "aparte.label": same("Apart\xE9 \xB7 {count}"),
@@ -24301,16 +24404,17 @@ var TEXTS = {
24301
24404
  vous: "Approchez-vous du cercle de l\u2019Apart\xE9 pour frapper."
24302
24405
  },
24303
24406
  "aparte.refused.unknown": same("Cet Apart\xE9 est termin\xE9."),
24304
- "controls.label": same("Barre de contr\xF4le"),
24305
24407
  "camera.label": same("Cam\xE9ra"),
24306
24408
  "camera.libre": same("Libre"),
24307
24409
  "camera.iso": same("Iso"),
24308
24410
  "camera.dessus": same("Dessus"),
24309
24411
  "camera.turnLeft": same("Tourner d\u2019un quart de tour \xE0 gauche (A)"),
24310
24412
  "camera.turnRight": same("Tourner d\u2019un quart de tour \xE0 droite (E)"),
24311
- "rings.toggle": same("Cercles de 2 m et 6 m"),
24312
- "panel.toggle": same("Panneau lat\xE9ral"),
24313
- "panel.tabs": same("Onglets du panneau"),
24413
+ "rings.toggle": same("Port\xE9e de voix"),
24414
+ "rings.hint": {
24415
+ tu: "Montre jusqu\u2019o\xF9 on t\u2019entend (2 m et 6 m)",
24416
+ vous: "Montre jusqu\u2019o\xF9 on vous entend (2 m et 6 m)"
24417
+ },
24314
24418
  "presents.title": same("Pr\xE9sents"),
24315
24419
  "presents.member": same("Membre"),
24316
24420
  "presents.guest": same("Invit\xE9"),
@@ -24325,6 +24429,56 @@ var TEXTS = {
24325
24429
  tu: "Le nom que les autres voient sur ton \xE9tiquette et dans la liste des Pr\xE9sents.",
24326
24430
  vous: "Le nom que les autres voient sur votre \xE9tiquette et dans la liste des Pr\xE9sents."
24327
24431
  },
24432
+ "settings.audio": same("Micro et sortie"),
24433
+ "settings.camera": same("Cam\xE9ra et \xE9cran"),
24434
+ "settings.camera.lead": {
24435
+ tu: "Ta cam\xE9ra est coup\xE9e \xE0 chaque entr\xE9e. Allum\xE9e, elle est visible de tout l\u2019Espace en petit, en grand \xE0 moins de 2 m ; dans une Salle ou un Apart\xE9, de ceux qui y sont seulement.",
24436
+ vous: "Votre cam\xE9ra est coup\xE9e \xE0 chaque entr\xE9e. Allum\xE9e, elle est visible de tout l\u2019Espace en petit, en grand \xE0 moins de 2 m ; dans une Salle ou un Apart\xE9, de ceux qui y sont seulement."
24437
+ },
24438
+ "settings.camera.device": same("Cam\xE9ra"),
24439
+ "settings.camera.default": same("Cam\xE9ra par d\xE9faut"),
24440
+ "settings.camera.atEntry": {
24441
+ tu: "Allumer ma cam\xE9ra en entrant",
24442
+ vous: "Allumer ma cam\xE9ra en entrant"
24443
+ },
24444
+ // Vidéo et partage d'écran (ADR 0009).
24445
+ "video.cameraOn": same("Allumer la cam\xE9ra"),
24446
+ "video.cameraOff": same("Couper la cam\xE9ra"),
24447
+ "video.cameraHint": same("Visible de tout l\u2019Espace en petit, en grand \xE0 moins de 2 m"),
24448
+ "video.cameraError.denied": {
24449
+ tu: "Cam\xE9ra refus\xE9e : autorise-la dans les r\xE9glages du navigateur.",
24450
+ vous: "Cam\xE9ra refus\xE9e : autorisez-la dans les r\xE9glages du navigateur."
24451
+ },
24452
+ "video.cameraError.unavailable": same("Cam\xE9ra indisponible"),
24453
+ "video.shareStart": same("Partager l\u2019\xE9cran"),
24454
+ "video.shareStop": same("Arr\xEAter le partage d\u2019\xE9cran"),
24455
+ "video.bandeau": same("Bandeau des vid\xE9os"),
24456
+ "video.more": same("Voir les {count} autres vid\xE9os"),
24457
+ "video.cameraMuted": same("cam\xE9ra coup\xE9e"),
24458
+ "video.screenOf": same("\xC9cran de {name}"),
24459
+ "video.myScreen": { tu: "Ton \xE9cran", vous: "Votre \xE9cran" },
24460
+ "video.mini": same("Vid\xE9o de {name}"),
24461
+ "video.enlarge": same("Agrandir"),
24462
+ "video.view": same("Vue Vid\xE9os"),
24463
+ "video.toggle": same("Espace ou Vue Vid\xE9os (V)"),
24464
+ "video.toggle.space": same("Espace"),
24465
+ "video.toggle.videos": same("Vid\xE9os"),
24466
+ "video.propose": same("Vue Vid\xE9os"),
24467
+ "video.later": same("Plus tard"),
24468
+ "video.back": same("Revenir \xE0 l\u2019Espace"),
24469
+ "video.around": { tu: "Autour de toi", vous: "Autour de vous" },
24470
+ "video.alone": {
24471
+ tu: "Personne \xE0 moins de 2 m. Approche-toi de quelqu\u2019un ou allume ta cam\xE9ra.",
24472
+ vous: "Personne \xE0 moins de 2 m. Approchez-vous de quelqu\u2019un ou allumez votre cam\xE9ra."
24473
+ },
24474
+ "video.mosaic": same("Revenir \xE0 la mosa\xEFque"),
24475
+ "video.fullscreen": same("Plein \xE9cran"),
24476
+ "video.fullscreenExit": same("Quitter le plein \xE9cran (\xC9chap)"),
24477
+ "video.zoomLevel": same("Niveau de zoom"),
24478
+ "video.fit": same("Ajuster"),
24479
+ "video.actualSize": same("Taille r\xE9elle (1:1)"),
24480
+ "video.zoomHint": same("Ctrl + molette ou pincement pour zoomer, glisser pour se d\xE9placer"),
24481
+ "settings.shortcuts": same("Raccourcis"),
24328
24482
  "settings.save": same("Enregistrer"),
24329
24483
  "settings.saved": same("Enregistr\xE9."),
24330
24484
  "settings.failed": {
@@ -24343,18 +24497,52 @@ var TEXTS = {
24343
24497
  "agents.status.waiting": same("Attend"),
24344
24498
  "agents.status.available": same("Disponible"),
24345
24499
  "agents.status.failed": same("En \xE9chec"),
24346
- "agents.offline": same("Hors ligne depuis {time}"),
24347
- "agents.pendingRequest": same("Demande en attente"),
24348
- "agents.pendingBadge": same("{count} demande(s) en attente"),
24500
+ "agents.status.review": same("\xC0 relire"),
24501
+ "agents.status.offline": same("Hors ligne"),
24502
+ "agents.filter": same("Quels Agents"),
24503
+ "agents.filter.all": same("Tous"),
24504
+ "agents.filter.mine": same("Les miens"),
24505
+ "agents.group.waiting": same("Attend une r\xE9ponse"),
24506
+ "agents.group.review": same("\xC0 relire"),
24507
+ "agents.group.failed": same("En \xE9chec"),
24508
+ "agents.group.working": same("Travaille"),
24509
+ "agents.group.available": same("Disponible"),
24510
+ "agents.group.offline": same("Hors ligne ({count})"),
24511
+ "agents.chip.waiting": same("R\xE9ponse"),
24512
+ // Demandes, À relire et Relais v2.
24513
+ "agents.answer": same("R\xE9pondre \xE0 \xAB {title} \xBB"),
24514
+ "agents.answer.other": same("Autre r\xE9ponse\u2026"),
24515
+ "agents.answer.open": same("R\xE9pondre\u2026"),
24516
+ "bubble.waiting": same("Attend une r\xE9ponse"),
24517
+ "bubble.review": same("\xC0 relire"),
24518
+ "bubble.approval": same("Approbation en attente"),
24519
+ "agents.exchange": same("\xAB {from} \xBB \xE9crit \xE0 \xAB {to} \xBB"),
24520
+ "relays.outdated": {
24521
+ tu: "Mets \xE0 jour ton Relais \xAB {names} \xBB : il est trop ancien pour ce Kather et ses Agents n\u2019apparaissent plus. Lance npx kather-relay@latest install-service sur cette machine.",
24522
+ vous: "Ce Relais est trop ancien pour ce Kather."
24523
+ },
24524
+ "protocol.outdated": {
24525
+ tu: "Kather a \xE9t\xE9 mis \xE0 jour : recharge la page pour continuer.",
24526
+ vous: "Kather a \xE9t\xE9 mis \xE0 jour : rechargez la page pour continuer."
24527
+ },
24528
+ "protocol.outdated.title": same("Nouvelle version de Kather"),
24529
+ "protocol.reload": same("Recharger"),
24530
+ "agents.chip.review": same("\xC0 relire"),
24531
+ "agents.chip.failed": same("\xC9chec"),
24532
+ "agents.mineEmpty": {
24533
+ tu: "Aucun de tes Agents n\u2019est dans cet Espace.",
24534
+ vous: "Aucun Agent."
24535
+ },
24349
24536
  "agents.noDesk": same("sans Poste"),
24350
- "agents.subagents": same("{active}/{total} sous-agents"),
24351
- "agents.alertBadge": same("{count} Agent(s) \xE0 voir"),
24537
+ "agents.where.comingToMe": { tu: "en route vers toi", vous: "en route vers vous" },
24538
+ "agents.where.coming": same("en route vers {name}"),
24539
+ "agents.where.nearMe": { tu: "pr\xE8s de toi", vous: "pr\xE8s de vous" },
24540
+ "agents.where.near": same("pr\xE8s de {name}"),
24541
+ "agents.where.returning": same("rentre \xE0 son Poste"),
24542
+ "agents.where.raised": same("main lev\xE9e \xE0 son Poste"),
24352
24543
  // Panneau d'un Agent (US 67 à 71, 76, 77).
24353
24544
  "agent.panel": same("Agent \xAB {title} \xBB"),
24354
- "agent.open": same("Ouvrir le panneau de l\u2019Agent"),
24355
24545
  "agent.prompt": same("Ouvrir \xAB {title} \xBB"),
24356
- "agent.close": same("Fermer le panneau"),
24357
- "agent.owner": same("Agent de {name}"),
24358
24546
  "agent.readOnly": same("Lecture seule \xB7 Agent de {name}"),
24359
24547
  "agent.restricted.private": same(
24360
24548
  "Agent priv\xE9 : seuls son Statut, son titre et son Propri\xE9taire sont visibles."
@@ -24380,6 +24568,7 @@ var TEXTS = {
24380
24568
  "agent.error.forbidden": same("Seul le Propri\xE9taire peut \xE9crire \xE0 cet Agent."),
24381
24569
  "agent.error.unknown-agent": same("Cet Agent n\u2019est plus l\xE0."),
24382
24570
  "agent.error.offline": same("Agent hors ligne : le Relais ou T3 Code ne r\xE9pond pas."),
24571
+ "agent.error.online": same("Cet Agent est de nouveau en ligne : il reste dans l\u2019Espace."),
24383
24572
  "agent.error.refused": same("Le Relais a refus\xE9 cette op\xE9ration."),
24384
24573
  "agent.error.escalation": {
24385
24574
  tu: "T3 Code refuse : ce thread tourne dans un mode plus large que l\u2019autorisation du Relais. Reconnecte ton T3 Code en Full access (kather-relay login).",
@@ -24391,13 +24580,30 @@ var TEXTS = {
24391
24580
  ),
24392
24581
  "agent.request": same("Demande en attente"),
24393
24582
  "agent.request.loading": same("Lecture de la demande\u2026"),
24394
- "agent.request.none": {
24395
- tu: "L\u2019Agent attend, mais pas une r\xE9ponse \xE0 une question : cette demande (une permission, sans doute) se traite dans T3 Code.",
24396
- vous: "L\u2019Agent attend une demande qui se traite dans T3 Code."
24397
- },
24583
+ "agent.request.reread": same("Relire la demande"),
24398
24584
  "agent.request.other": same("Autre r\xE9ponse"),
24399
24585
  "agent.request.answer": same("R\xE9pondre"),
24400
24586
  "agent.request.answered": same("R\xE9ponse envoy\xE9e."),
24587
+ "agent.menu": same("Plus d\u2019actions"),
24588
+ "agent.goToDesk": same("Aller \xE0 son Poste"),
24589
+ "agent.meta.mine": { tu: "\xE0 toi", vous: "\xE0 vous" },
24590
+ "agent.meta.of": same("de {name}"),
24591
+ "agent.offline.machine": same("La machine {machine} ne donne plus de nouvelles depuis {time}."),
24592
+ "agent.offline.unknown": same("Sa machine ne donne plus de nouvelles depuis {time}."),
24593
+ "agent.offline.back": same("Son fil reviendra quand son Relais sera relanc\xE9."),
24594
+ "agent.remove": same("Retirer de l\u2019Espace"),
24595
+ "agent.remove.hint": same(
24596
+ "Il dispara\xEEt pour tous et lib\xE8re son Poste ; s\u2019il revient en ligne, il r\xE9appara\xEEt comme un nouvel Agent."
24597
+ ),
24598
+ "agent.removed": same("\xAB {title} \xBB a \xE9t\xE9 retir\xE9 de l\u2019Espace."),
24599
+ "agent.request.waiting": {
24600
+ tu: "L\u2019Agent attend ta r\xE9ponse",
24601
+ vous: "L\u2019Agent attend une r\xE9ponse"
24602
+ },
24603
+ "agent.request.collapse": same("Replier la demande"),
24604
+ "agent.request.expand": same("D\xE9plier la demande"),
24605
+ "agent.request.approve": same("\xC0 approuver dans T3 Code"),
24606
+ "agent.composer": same("\xC9crire \xE0 l\u2019Agent\u2026 (Ctrl+Entr\xE9e)"),
24401
24607
  "agent.private": same("Priv\xE9"),
24402
24608
  "agent.private.hint": {
24403
24609
  tu: "Les autres Membres ne voient que son Statut et son titre, sans son fil.",
@@ -24425,7 +24631,7 @@ var TEXTS = {
24425
24631
  tu: "Reconnecte ton T3 Code : l\u2019autorisation de \xAB {machine} \xBB a expir\xE9. Lance kather-relay login sur cette machine.",
24426
24632
  vous: "Reconnectez votre T3 Code : l\u2019autorisation de \xAB {machine} \xBB a expir\xE9."
24427
24633
  },
24428
- "settings.relays": same("Relais"),
24634
+ "settings.relays": same("Relais et Agents"),
24429
24635
  "relays.lead": {
24430
24636
  tu: "Le Relais fait appara\xEEtre tes Agents T3 Code dans Kather. Il tourne sur ta machine : ton jeton T3 Code ne la quitte jamais.",
24431
24637
  vous: "Le Relais fait appara\xEEtre vos Agents T3 Code dans Kather. Il tourne sur votre machine."
@@ -24436,7 +24642,7 @@ var TEXTS = {
24436
24642
  vous: "Collez-le dans votre T3 Code : votre agent installe, appaire et lance le Relais."
24437
24643
  },
24438
24644
  "relays.prompt": same(
24439
- "Installe et lance le Relais Kather sur cette machine. 1) V\xE9rifie que Node 22 ou plus est install\xE9. 2) Lance `npx kather-relay connect {url}` : il m\u2019affichera un code que je confirmerai dans le navigateur, puis il ouvrira la page de consentement de T3 Code, o\xF9 je choisirai Full access. 3) Installe ensuite le Relais en service utilisateur qui d\xE9marre avec ma session (systemd `--user` sous Linux, launchd sous macOS) avec `npx kather-relay install-service`, puis v\xE9rifie qu\u2019il tourne avec `npx kather-relay status`. Ne modifie aucun autre r\xE9glage de T3 Code."
24645
+ "Installe et lance le Relais Kather sur cette machine. 1) V\xE9rifie que Node 22 ou plus est install\xE9. 2) Lance `npx kather-relay@latest connect {url}` : il m\u2019affichera un code que je confirmerai dans le navigateur, puis il ouvrira la page de consentement de T3 Code, o\xF9 je choisirai Full access. 3) Installe ensuite le Relais en service utilisateur qui d\xE9marre avec ma session (systemd `--user` sous Linux, launchd sous macOS) avec `npx kather-relay@latest install-service`, puis v\xE9rifie qu\u2019il tourne avec `npx kather-relay@latest status`. Ne modifie aucun autre r\xE9glage de T3 Code."
24440
24646
  ),
24441
24647
  "relays.prompt.copy": same("Copier le prompt"),
24442
24648
  "relays.prompt.copied": same("Copi\xE9."),
@@ -24549,6 +24755,9 @@ var TEXTS = {
24549
24755
  "invitations.pending": same("Espace {space} \xB7 lien valable jusqu\u2019au {date}"),
24550
24756
  "invitations.entered": same("Espace {space} \xB7 Entr\xE9e, acc\xE8s jusqu\u2019\xE0 minuit"),
24551
24757
  "invitations.revoke": same("R\xE9voquer"),
24758
+ "invitations.copy": same("Copier le lien"),
24759
+ "invitations.copied": same("Lien copi\xE9."),
24760
+ "invitations.fresh": same("Envoy\xE9e \xE0 l\u2019instant"),
24552
24761
  "invitations.revoke.confirm": same("R\xE9voquer l\u2019invitation de {email} ?"),
24553
24762
  "invitation.title": {
24554
24763
  tu: "{inviter} t\u2019invite dans l\u2019Espace {space}",
@@ -24773,7 +24982,11 @@ var threadSchema = external_exports.looseObject({
24773
24982
  link: external_exports.string().nullish(),
24774
24983
  settled: external_exports.boolean().nullish(),
24775
24984
  parentThreadId: external_exports.string().nullish(),
24776
- relationshipToParent: external_exports.string().nullish()
24985
+ relationshipToParent: external_exports.string().nullish(),
24986
+ /** Change à chaque nouvel élément de la chronologie. */
24987
+ updatedAt: external_exports.string().nullish(),
24988
+ /** Tous les éléments de la chronologie, activité comprise. */
24989
+ itemCount: external_exports.number().int().nullish()
24777
24990
  });
24778
24991
  var threadPageSchema = external_exports.looseObject({
24779
24992
  threads: external_exports.array(threadSchema),
@@ -24797,7 +25010,11 @@ var threadReadSchema = external_exports.looseObject({
24797
25010
  type: external_exports.string(),
24798
25011
  text: external_exports.string().nullish(),
24799
25012
  textTruncated: external_exports.boolean().nullish(),
24800
- updatedAt: external_exports.string().nullish()
25013
+ updatedAt: external_exports.string().nullish(),
25014
+ /** `agent` pour un message écrit par un autre fil, et pour les réponses de l'Agent. */
25015
+ createdBy: external_exports.string().nullish(),
25016
+ /** Le fil qui a écrit ce `user_message`, quand c'est un Agent. */
25017
+ senderThreadId: external_exports.string().nullish()
24801
25018
  })
24802
25019
  ),
24803
25020
  nextPosition: external_exports.number().int().nullish(),
@@ -24887,11 +25104,29 @@ function createT3(options) {
24887
25104
  }
24888
25105
 
24889
25106
  // src/version.ts
24890
- var RELAY_VERSION = "0.1.0";
25107
+ var RELAY_VERSION = "0.2.0";
24891
25108
 
24892
25109
  // src/relay.ts
24893
25110
  var EXPIRY_WARNING_MS = 3 * 24 * 60 * 60 * 1e3;
24894
25111
  var PROJECT_CACHE_MS = 6e4;
25112
+ var EXCHANGE_READ_ITEMS = 5;
25113
+ var EXCHANGE_MAX_PAGES = 20;
25114
+ async function readSince(client, threadId, after, added) {
25115
+ const items = [];
25116
+ const pages = added === null ? EXCHANGE_MAX_PAGES : Math.ceil(added / EXCHANGE_READ_ITEMS) + 1;
25117
+ let position = after;
25118
+ for (let page = 0; page < pages; page++) {
25119
+ const read = await client.readThread(threadId, {
25120
+ limit: EXCHANGE_READ_ITEMS,
25121
+ maxCharsPerItem: 1,
25122
+ ...position >= 0 ? { afterPosition: position } : {}
25123
+ });
25124
+ items.push(...read.items);
25125
+ if (!read.hasMore || read.nextPosition == null) break;
25126
+ position = read.nextPosition;
25127
+ }
25128
+ return items;
25129
+ }
24895
25130
  var MAX_RECONNECT_MS = 3e4;
24896
25131
  var serialize = (agent) => JSON.stringify(agent);
24897
25132
  function startRelay(options) {
@@ -24914,6 +25149,8 @@ function startRelay(options) {
24914
25149
  const watched = /* @__PURE__ */ new Map();
24915
25150
  let projects = null;
24916
25151
  let lastPending = /* @__PURE__ */ new Map();
25152
+ let requests = /* @__PURE__ */ new Map();
25153
+ let timelines = /* @__PURE__ */ new Map();
24917
25154
  const connectT3 = (from) => from ? createT3({
24918
25155
  url: from.url,
24919
25156
  accessToken: from.accessToken,
@@ -24965,6 +25202,62 @@ function startRelay(options) {
24965
25202
  wake = done;
24966
25203
  });
24967
25204
  const pollSoon = () => wake?.();
25205
+ async function readRequests(client, waiting, pending) {
25206
+ const next = /* @__PURE__ */ new Map();
25207
+ await Promise.all(
25208
+ waiting.map(async (t) => {
25209
+ const count = pending.get(t.threadId) ?? 0;
25210
+ if (count === 0) return;
25211
+ const key = `${count}:${t.updatedAt ?? ""}`;
25212
+ const known = requests.get(t.threadId);
25213
+ if (known?.key === key) return void next.set(t.threadId, known);
25214
+ try {
25215
+ const listed = pendingRequestsFrom({ requests: await client.pendingRequests(t.threadId) });
25216
+ const first = listed?.[0];
25217
+ const request = !listed ? known?.request ?? null : first ? { kind: "question", request: first } : { kind: "approval" };
25218
+ next.set(t.threadId, { key: listed ? key : "", request });
25219
+ } catch (error62) {
25220
+ if (error62 instanceof T3Unauthorized || error62 instanceof T3Unreachable) throw error62;
25221
+ log(`demande du thread ${t.threadId} : ${String(error62)}`);
25222
+ next.set(t.threadId, { key: "", request: known?.request ?? null });
25223
+ }
25224
+ })
25225
+ );
25226
+ requests = next;
25227
+ }
25228
+ async function findExchanges(client, threads) {
25229
+ const next = /* @__PURE__ */ new Map();
25230
+ const changed = [];
25231
+ for (const t of threads) {
25232
+ if (!knownAgents.has(t.threadId)) continue;
25233
+ const current = { updatedAt: t.updatedAt ?? null, itemCount: t.itemCount ?? null };
25234
+ next.set(t.threadId, current);
25235
+ const before = timelines.get(t.threadId);
25236
+ if (!before || before.updatedAt === current.updatedAt || before.itemCount === null) continue;
25237
+ if (current.itemCount !== null && current.itemCount <= before.itemCount) continue;
25238
+ const added = current.itemCount === null ? null : current.itemCount - before.itemCount;
25239
+ changed.push({ threadId: t.threadId, after: before.itemCount - 1, added });
25240
+ }
25241
+ timelines = next;
25242
+ const reads = await Promise.allSettled(
25243
+ changed.map(({ threadId, after, added }) => readSince(client, threadId, after, added))
25244
+ );
25245
+ reads.forEach((read, i) => {
25246
+ const toThreadId = changed[i].threadId;
25247
+ if (read.status === "rejected") {
25248
+ if (read.reason instanceof T3Unauthorized || read.reason instanceof T3Unreachable) {
25249
+ throw read.reason;
25250
+ }
25251
+ return log(`\xE9changes du thread ${toThreadId} : ${String(read.reason)}`);
25252
+ }
25253
+ for (const item2 of read.value) {
25254
+ const from = item2.senderThreadId;
25255
+ if (item2.type !== "user_message" || item2.createdBy !== "agent" || !from) continue;
25256
+ if (from === toThreadId || !knownAgents.has(from)) continue;
25257
+ send({ type: "agents.exchange", fromThreadId: from, toThreadId });
25258
+ }
25259
+ });
25260
+ }
24968
25261
  function agentsOf(threads, pending) {
24969
25262
  const subagents = /* @__PURE__ */ new Map();
24970
25263
  for (const t of threads) {
@@ -24983,7 +25276,7 @@ function startRelay(options) {
24983
25276
  projectId: t.projectId ?? null,
24984
25277
  projectName: t.projectId ? projects?.names.get(t.projectId) ?? null : null,
24985
25278
  status: katherStatusOf(t.status, pendingCount),
24986
- pendingRequest: pendingCount > 0,
25279
+ request: pendingCount > 0 ? requests.get(t.threadId)?.request ?? null : null,
24987
25280
  subagents: subagents.get(t.threadId) ?? { active: 0, total: 0 },
24988
25281
  link: t.link ?? null
24989
25282
  };
@@ -25032,7 +25325,9 @@ function startRelay(options) {
25032
25325
  projects = { at: now(), names: await t3.projectNames() };
25033
25326
  }
25034
25327
  const client = t3;
25035
- const waiting = threads.filter((t) => knownAgents.has(t.threadId) && t.status === "waiting");
25328
+ const waiting = threads.filter(
25329
+ (t) => knownAgents.has(t.threadId) && (t.status === "waiting" || t.status === "running")
25330
+ );
25036
25331
  const details = await Promise.allSettled(
25037
25332
  waiting.map((t) => client.readThread(t.threadId, { limit: 1, maxCharsPerItem: 1 }))
25038
25333
  );
@@ -25050,8 +25345,10 @@ function startRelay(options) {
25050
25345
  pending.set(t.threadId, lastPending.get(t.threadId) ?? 0);
25051
25346
  });
25052
25347
  lastPending = pending;
25348
+ await readRequests(client, waiting, pending);
25053
25349
  setAuth(expiryState());
25054
25350
  publish(agentsOf(threads, pending));
25351
+ await findExchanges(client, threads);
25055
25352
  for (const threadId of [...watched.keys()]) await sendThread(threadId);
25056
25353
  } catch (error62) {
25057
25354
  if (error62 instanceof T3Unauthorized) setAuth("expired");
@@ -25277,7 +25574,10 @@ function serviceDefinition(options) {
25277
25574
  ].join("\n"),
25278
25575
  install: [
25279
25576
  ["systemctl", "--user", "daemon-reload"],
25280
- ["systemctl", "--user", "enable", "--now", SERVICE_NAME]
25577
+ ["systemctl", "--user", "enable", SERVICE_NAME],
25578
+ // `restart` démarre un service arrêté et relance un service en cours : une mise à jour
25579
+ // (`npx kather-relay@latest install-service`) repart sur la nouvelle copie.
25580
+ ["systemctl", "--user", "restart", SERVICE_NAME]
25281
25581
  ],
25282
25582
  uninstall: [["systemctl", "--user", "disable", "--now", SERVICE_NAME]]
25283
25583
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "kather-relay",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "Le Relais de Kather : fait apparaître les Agents de ton T3 Code dans ton Espace Kather.",
5
5
  "license": "MIT",
6
6
  "homepage": "https://kather.katalyx.fr",