clavarium 0.2.375-next.1 → 0.2.378-next.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "clavarium",
3
- "version": "0.2.375-next.1",
3
+ "version": "0.2.378-next.1",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "clavarium",
9
- "version": "0.2.375-next.1",
9
+ "version": "0.2.378-next.1",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@xterm/addon-fit": "^0.11.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clavarium",
3
- "version": "0.2.375-next.1",
3
+ "version": "0.2.378-next.1",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "description": "A conversation is a VM with a terminal in it. Everything else is a folder.",
package/server/main.mjs CHANGED
@@ -718,6 +718,8 @@ export async function startHome({ port, host, dataDir, mntDir = dataDir, gitRese
718
718
  } });
719
719
  rooms.boxesBound(boxes.all());
720
720
  rooms.refresh();
721
+ /* A Tether call held for an AI at work for a room: said in that room too, closed there once decided (tethers.mjs). */
722
+ tethers.rooms = { held: (name, event) => rooms.held(name, event), closed: (ids, event) => rooms.closed(ids, event) };
721
723
  /* FEATURES (features.mjs): read from what the modules say, never a secret's value. The remote screen and the
722
724
  catalogue are slow to ask: kept a while, and never waited for long. */
723
725
  const screenNow = cached(() => meshcentral.status().catch(() => null), 60_000);
package/server/rooms.mjs CHANGED
@@ -30,6 +30,11 @@ export const HOPS_MAX = 8;
30
30
  /* How long an AI given a message is said to be thinking, at most (it may never answer). */
31
31
  export const THINKING_MS = 10 * 60_000;
32
32
  const KINDS = ['person', 'ai', 'box'];
33
+ /* A ROOM'S OWN LINES (a Tether call held for one of its AIs, and what closes it): the kinds, their level, at most kept. */
34
+ const LINE_LEVELS = Object.freeze({ 'tether-demande': 'needs-you', 'tether-done': 'trace' });
35
+ const LEVELS = ['needs-you', 'news', 'trace'];
36
+ const LINE_ACTION = /^[a-z]{1,20}(:[a-z0-9][a-z0-9._-]{0,63})?$/;
37
+ export const ROOM_LINES_MAX = 50;
33
38
  /* A FILE THE PERSON ATTACHES IN A ROOM (06-11: a room carries what the chat carries): kept in the room's own folder
34
39
  (rooms/<id>/attached/<uuid>/<name>), named in its thread by ROOM_FILE's path; a copy goes to each AI the message
35
40
  goes to, attached in its own chat as the person attaches one there (talk.attach) -- never to the others. */
@@ -56,6 +61,11 @@ const nameOf = (v) => String(v ?? '').replace(/\s+/g, ' ').trim().slice(0, ROOM_
56
61
  a secret never passes from one AI to another through a room. */
57
62
  export const masked = (text) => { const str = String(text ?? ''); const hits = scan(str); if (!hits.length) return str; let out = ''; let last = 0; for (const h of hits) { out += str.slice(last, h.start) + '••••••'; last = h.end; } return out + str.slice(last); };
58
63
 
64
+ /* Every text of a line masked (a card's title, facts, quote...: plain data, words of the screen and their values) --
65
+ never a button's id nor a word's key. */
66
+ const maskedAll = (v, depth = 0) => (typeof v === 'string' ? masked(v) : depth > 8 || v === null || typeof v !== 'object' ? v
67
+ : Array.isArray(v) ? v.map((x) => maskedAll(x, depth + 1)) : Object.fromEntries(Object.entries(v).map(([k, x]) => [k, k === 'id' || k === 'k' ? x : maskedAll(x, depth + 1)])));
68
+
59
69
  /* A sign as the cards draw it (web/kit.js hueOf, initialOf): the same name, the same letter and colour. */
60
70
  export const lookOf = (name) => { const text = String(name ?? ''); let h = 0; for (const ch of text) h = (h * 31 + ch.codePointAt(0)) >>> 0; return { initial: ([...text.trim()][0] ?? '?').toUpperCase(), hue: h % 360 }; };
61
71
 
@@ -171,6 +181,10 @@ export class Rooms {
171
181
  if (options?.seen !== false) { const list = this.#all(); const r = list.find((x) => x.id === id); if (r) { r.seen = this.#now().toISOString(); this.#file.set(list); } }
172
182
  const thinking = this.#thinkingOf(id, members); const doing = this.#doingOf(id, members);
173
183
  const messages = (read.messages ?? []).map((m) => this.#opened(m, members)).map((m) => (typeof m.text === 'string' && m.text.includes('@{') ? { ...m, text: named(m.text) } : m));
184
+ /* Its lines (a Tether call of one of its AIs, held): among its messages, where they happened -- those of the time read. */
185
+ const from = read.window?.more > 0 ? String(messages[0]?.at ?? '') : '';
186
+ const lines = (Array.isArray(room.lines) ? room.lines : []).filter((l) => String(l.at) >= from).map((l) => ({ from: 'system', kind: 'system', choices: [], ...l }));
187
+ if (lines.length) { messages.push(...lines); messages.sort((a, b) => String(a.at).localeCompare(String(b.at))); }
174
188
  /* The files of the thread (shown, cited): each as the message that has it opens it; one nobody opens here, not listed. */
175
189
  const opens = new Map(); for (const m of messages) for (const f of [...(m.refs ?? []), ...(m.files ?? []), ...(m.open ? [m] : [])]) if (f.path && f.open) opens.set(f.path, f.open);
176
190
  const files = Array.isArray(read.files) ? read.files.map((f) => (String(f.open ?? '').startsWith(CITE) ? (opens.has(f.path) ? { ...f, open: opens.get(f.path) } : null) : f)).filter(Boolean) : read.files;
@@ -387,6 +401,41 @@ export class Rooms {
387
401
  return copies;
388
402
  }
389
403
 
404
+ /* A TETHER CALL HELD FOR AN AI AT WORK FOR A ROOM (07-11): an AI given a message of the room and not done with it
405
+ asks the person to allow a call -- its card goes in that room's thread too (and each room it works for), the
406
+ same line its own chat has (talk.event's shape: Accept / Refuse, Parley's request card), named by the AI.
407
+ Kept apart from the thread (the room's `lines`, ROOM_LINES_MAX), read among its messages; masked as any line
408
+ of a room. Gives the rooms it went to: what is said once it is decided goes there (closed). */
409
+ held(conversation, event) {
410
+ const list = this.#all(); const now = Date.now(); const ids = [];
411
+ for (const r of list) {
412
+ const on = this.#thinking.get(r.id);
413
+ const me = on && this.#shown(r).find((m) => m.kind === 'ai' && m.ref === conversation && on.has(m.id) && now - on.get(m.id).at < THINKING_MS);
414
+ if (!me) continue;
415
+ const request = event?.request && typeof event.request === 'object' ? { ...event.request, sub: [me.name, ...(Array.isArray(event.request.sub) ? event.request.sub : event.request.sub ? [event.request.sub] : [])].slice(0, 4) } : undefined;
416
+ const text = event?.text && typeof event.text === 'object' ? event.text : `${me.name}: ${String(event?.text ?? '')}`;
417
+ if (this.#line(r, { ...event, text, ...(request ? { request } : {}) })) ids.push(r.id);
418
+ }
419
+ if (ids.length) this.#file.set(list);
420
+ return ids;
421
+ }
422
+ /* What closes it (a 'tether-done' carrying its id), in the rooms its card went to. */
423
+ closed(ids, event) {
424
+ const list = this.#all(); let any = false;
425
+ for (const r of list) if ((Array.isArray(ids) ? ids : []).includes(r.id) && this.#line(r, event)) any = true;
426
+ if (any) this.#file.set(list);
427
+ }
428
+ #line(room, { kind, text, level, tone, topic, brief, actions, request, outcome } = {}) {
429
+ if (!Object.hasOwn(LINE_LEVELS, kind)) return null;
430
+ const kept = { id: randomUUID(), at: this.#now().toISOString(), event: kind, text: maskedAll(text), level: LEVELS.includes(level) ? level : LINE_LEVELS[kind],
431
+ ...(tone === 'warn' || tone === 'bad' ? { tone } : {}), ...(topic ? { topic: maskedAll(topic) } : {}), ...(typeof brief === 'string' && brief ? { brief: brief.slice(0, 60) } : {}),
432
+ actions: (Array.isArray(actions) ? actions : []).filter((a) => typeof a === 'string' && LINE_ACTION.test(a)).slice(0, 4),
433
+ ...(request && typeof request === 'object' && JSON.stringify(request).length <= 8000 ? { request: maskedAll(request) } : {}),
434
+ ...(typeof outcome === 'string' && outcome ? { outcome: outcome.slice(0, 20) } : {}) };
435
+ room.lines = [...(Array.isArray(room.lines) ? room.lines : []), kept].slice(-ROOM_LINES_MAX);
436
+ return kept;
437
+ }
438
+
390
439
  /* Each conversation told the rooms it is in, and who is there. */
391
440
  #tellConversations(list) {
392
441
  const shown = list.map((r) => ({ ...r, members: this.#shown(r) }));
@@ -40,15 +40,15 @@ export const SETTINGS_WORDS_FR = {
40
40
  'set-backup-now': 'Exporter · importer', 'set-git-key': '1 clé git', 'set-git-keys': '{n} clés git', 'set-restart': 'redémarrer',
41
41
  'set-logs-now': 'Clavarium, machines, système',
42
42
  'set-dict-backup': 'Secours en service', 'set-dict-expired': 'Clé expirée', 'set-dict-with-backup': 'avec secours', 'set-dict-server': 'réglée sur le serveur',
43
- 'set-attached': '{n} joints', 'set-secret': '1 secret', 'set-secrets': '{n} secrets', 'set-offline': 'hors ligne',
43
+ 'set-offline': 'hors ligne',
44
44
  'set-device': '1 appareil', 'set-devices': '{n} appareils', 'set-no-device': 'Aucun appareil', 'set-chat': 'Chat',
45
45
  'set-wakeups-on': '{on} sur {n} actifs', 'set-asleep': 'En veille', 'set-not-looked': 'Pas encore regardée',
46
46
  'set-running-well': 'En marche · rien d\'anormal', 'set-conv-advanced': 'Archiver · remettre à neuf · supprimer',
47
47
  /* New asset, one box (05-11, step 3): one verb per gesture -- Créer, Attacher, Terminé. */
48
- 'set-na-new': 'Nouvel asset', 'set-na-attach': 'Attacher', 'set-na-there': 'Déjà là', 'set-na-or-new': 'ou un nouveau',
48
+ 'set-na-new': 'Nouvel asset', 'set-na-there': 'Déjà là', 'set-na-or-new': 'ou un nouveau',
49
49
  'set-na-step-kind': 'Type', 'set-na-step-set': 'Réglage', 'set-na-step-made': 'Créé', 'set-na-back': 'Retour',
50
- 'set-na-will': 'Sera relié à {name}', 'set-na-level': 'Ce qu\'il pourra faire', 'set-na-read': 'Lecture', 'set-na-write': 'Écriture',
51
- 'set-na-made': '{name} créé', 'set-na-made-to': '{name} créé et relié à {to}', 'set-na-another': 'En créer un autre', 'set-na-done': 'Terminé',
50
+ 'set-na-will': 'Sera donné à {name}', 'set-na-level': 'Ce qu\'il pourra faire',
51
+ 'set-na-made': '{name} créé', 'set-na-made-to': '{name} créé et donné à {to}', 'set-na-another': 'En créer un autre', 'set-na-done': 'Terminé',
52
52
  'set-na-create': 'Créer', 'set-na-new-key': '+ Nouvelle clé…',
53
53
  /* The top bar's way back (06-11, kit.js TopBar). */
54
54
  'set-back': 'Retour', 'set-back-convs': 'Conversations',
@@ -101,7 +101,7 @@ export const SETTINGS_WORDS_FR = {
101
101
  'set-as-nobody': 'personne',
102
102
  /* The one list of assets (06-11, kit AssetList): its views, its one button, a group's head, what an empty one says. */
103
103
  'set-as-new': 'Nouveau', 'set-as-by-group': 'Groupes', 'set-as-by-type': 'Types', 'set-as-no-group': 'Sans groupe',
104
- 'set-as-fold': 'Replier', 'set-as-unfold': 'Déplier', 'set-as-group-open': 'Réglages de {name}', 'set-as-group-gear': 'Donner, renommer, supprimer',
104
+ 'set-as-fold': 'Replier', 'set-as-unfold': 'Déplier', 'set-as-group-gear': 'Donner, renommer, supprimer',
105
105
  'set-as-group-empty': 'Vide : glissez un asset ici', 'set-as-all-grouped': 'Chaque asset est dans un groupe',
106
106
  'set-as-no-groups': 'Créez un groupe, puis glissez-y un asset',
107
107
  /* One asset opened (06-11, kit AssetPanel, AccessList): who has it, its kind, what a calendar's levels mean, delete. */
@@ -140,7 +140,6 @@ export const SETTINGS_WORDS_FR = {
140
140
  'set-as-card': '1 carte', 'set-as-cards': '{n} cartes', 'set-as-linked': 'lié : {list}',
141
141
  'set-as-to-send': 'À envoyer', 'set-as-to-send-sub': 'tous les dépôts, relus et envoyés ensemble', 'set-as-waiting': '{n} vous attendent',
142
142
  'set-as-none-given': 'Rien ne lui est encore donné.', 'set-as-no-match': 'Rien à ce nom.',
143
- 'set-as-group': 'Groupe', 'set-as-group-make': 'Créez-en un dans la vue « Groupes ».',
144
143
  'set-as-files': 'Fichiers', 'set-as-browse': 'Parcourir',
145
144
  'set-cx-mail': 'boîte mail', 'set-cx-calendar': 'agenda', 'set-cx-contacts': 'carnet d\'adresses', 'set-cx-files': 'dossier en ligne',
146
145
  /* External MCP servers (mcpx.mjs): the form, their tools, the rights in their own words. */
@@ -160,9 +159,21 @@ export const SETTINGS_WORDS_FR = {
160
159
  'set-rt-mcp-hint-propose': 'Tous les outils, chaque modification approuvée', 'set-rt-mcp-hint-free': 'Tous les outils ; suppressions demandées',
161
160
  'set-rt-mcp-hint-frame': 'Votre choix pour chaque sorte d\'outil',
162
161
  'set-cx-ssh': 'machine', 'set-cx-computer': 'ordinateur', 'set-cx-browser': 'navigateur', 'set-cx-screen': 'écran', 'set-cx-device': 'appareil',
163
- 'set-acc-share': 'Partager avec un appareil…', 'set-acc-in-assets': 'Ouvrir dans Assets', 'set-acc-detach': 'Retirer',
162
+ 'set-acc-share': 'Partager avec un appareil…', 'set-acc-in-assets': 'Ouvrir dans Assets',
164
163
  'set-acc-used-1': 'Utilisé par 1 conversation : mettez « Aucun »', 'set-acc-used-n': 'Utilisé par {n} conversations : mettez « Aucun »',
165
- 'set-acc-folders': 'Dossiers, dépôts, boîtes mail', 'set-acc-secrets': 'Secrets remis à son outil',
164
+ /* Give… (07-11, assets step 4): in a conversation's tab; Settings › Access one line; a secret's next start. */
165
+ 'set-give-go': 'Donner', 'set-give-all': 'Elle a déjà chaque asset.',
166
+ /* A group (07-11, assets step 5): opened in the asset panel, given to every member, made from any view. */
167
+ 'set-group-new': 'Nouveau groupe', 'set-group-new-dots': 'Nouveau groupe…', 'set-group-name': 'Nom du groupe',
168
+ 'set-group-add': 'Ajouter', 'set-group-cancel': 'Annuler', 'set-group-of': 'Groupe de {name}',
169
+ 'set-group-in': 'Dans le groupe', 'set-group-has': '{has} sur {of}', 'set-group-per-asset': 'Le droit reste sur chaque asset.',
170
+ 'set-group-empty-hint': 'Vide : choisissez ce groupe sur un asset.',
171
+ 'set-group-rename': 'Renommer…', 'set-group-save': 'Renommer', 'set-group-remove': 'Retirer le groupe…',
172
+ 'set-group-remove-q': 'Ses assets restent ; seul le groupe part.', 'set-group-remove-yes': 'Retirer',
173
+ 'set-group-skipped-1': '1 ignoré : {why}', 'set-group-skipped-n': '{n} ignorés : {why}',
174
+ 'set-group-only': '{kind}, {levels} seulement', 'set-group-no-device': '{kind}, pas pour un appareil',
175
+ 'set-acc-assets-1': '1 asset · voir', 'set-acc-assets-n': '{n} assets · voir', 'set-acc-asset-1': '1 asset', 'set-acc-asset-n': '{n} assets',
176
+ 'set-secret-next-on': '{name} : remis à son outil au prochain démarrage', 'set-secret-next-off': '{name} : plus remis dès son prochain démarrage',
166
177
  'set-files-list': 'Liste', 'set-files-icons': 'Icônes', 'set-files-empty': 'Ce dossier est vide.',
167
178
  'set-files-read-only': 'Lecture seule : rien ne peut y être changé.',
168
179
  'set-this-conv': 'Cette conversation · {name}', 'set-ask-first': 'Demander avant d\'agir',
@@ -103,6 +103,9 @@ export class Tethers {
103
103
  /* Set by the server once the thread is ready (main.mjs): (name, event) => talk.event(...).
104
104
  A call waiting for the owner is said in its conversation's thread when there is one. */
105
105
  announce = null;
106
+ /* Set by the server too (main.mjs): the rooms (rooms.mjs) -- a call held for an AI at work for a room is said in
107
+ that room as well (held: the rooms it went to), and closed there (closed) when it is decided. */
108
+ rooms = null;
106
109
 
107
110
  /* `resources`: Resources (workFolder). `conversations()`: the names of the conversations.
108
111
  `secrets`: the vault, read by name for a connector only (reveal). `dockets`: { on(name), post(name,
@@ -152,7 +155,10 @@ export class Tethers {
152
155
  /* `request`: the card Parley draws (demandes.mjs); the text and its buttons stay for an older screen. */
153
156
  let request;
154
157
  try { request = demandeView(r); } catch { request = undefined; }
155
- try { this.announce(name, { kind: 'tether-demande', text: text || (r.verb ?? 'A Tether call'), topic: TOPIC_REQUEST, tone: 'warn', actions: [`allow:${r.id}`, `deny:${r.id}`], ...(request ? { request } : {}) }); } catch { /* its chat is off */ }
158
+ const event = { kind: 'tether-demande', text: text || (r.verb ?? 'A Tether call'), topic: TOPIC_REQUEST, tone: 'warn', actions: [`allow:${r.id}`, `deny:${r.id}`], ...(request ? { request } : {}) };
159
+ try { this.announce(name, event); } catch { /* its chat is off */ }
160
+ /* At work for a room (a message of it not yet answered): the same card there, decided there as here. */
161
+ try { const ids = this.rooms?.held(name, event); if (Array.isArray(ids) && ids.length) this.#seen.get(r.id).rooms = ids; } catch { /* its rooms' */ }
156
162
  }
157
163
  /* An approved call only the owner's browser can do (a device to flash): said once, with where to do it. */
158
164
  let jobs = [];
@@ -178,7 +184,9 @@ export class Tethers {
178
184
  const accepted = outcome === 'yes' || outcome === 'always';
179
185
  if (accepted) { this.#acting.push({ id, ...info }); if (this.#acting.length > ACTING_MAX) this.#acting.shift(); }
180
186
  const text = endedLine(info.verb, outcome);
181
- try { this.announce(info.name, { kind: 'tether-done', text, topic: TOPIC_REQUEST, brief: id, ...(outcome ? { outcome } : {}) }); } catch { /* its chat is off */ }
187
+ const done = { kind: 'tether-done', text, topic: TOPIC_REQUEST, brief: id, ...(outcome ? { outcome } : {}) };
188
+ try { this.announce(info.name, done); } catch { /* its chat is off */ }
189
+ if (info.rooms) try { this.rooms?.closed(info.rooms, done); } catch { /* its rooms' */ }
182
190
  }
183
191
  for (const id of [...this.#ended.keys()]) if (!this.#seen.has(id)) this.#ended.delete(id);
184
192
  }