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.
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/server/main.mjs +2 -0
- package/server/rooms.mjs +49 -0
- package/server/settingswords.mjs +19 -8
- package/server/tethers.mjs +10 -2
- package/web/app.js +275 -277
- package/web/index.html +4 -19
- package/web/kit.js +205 -28
- package/web/links.js +2 -2
- package/web/style.css +41 -35
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "clavarium",
|
|
3
|
-
"version": "0.2.
|
|
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.
|
|
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
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) }));
|
package/server/settingswords.mjs
CHANGED
|
@@ -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-
|
|
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-
|
|
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
|
|
51
|
-
'set-na-made': '{name} créé', 'set-na-made-to': '{name} créé et
|
|
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-
|
|
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',
|
|
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
|
-
|
|
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',
|
package/server/tethers.mjs
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
}
|