@mmmbuto/nexuscrew 0.8.37 → 0.8.38

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.
@@ -11,7 +11,7 @@
11
11
  <meta name="apple-mobile-web-app-title" content="NexusCrew" />
12
12
  <link rel="manifest" href="/manifest.json" />
13
13
  <title>NexusCrew</title>
14
- <script type="module" crossorigin src="/assets/index-BU0QZe1s.js"></script>
14
+ <script type="module" crossorigin src="/assets/index-0uO9YgxK.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="/assets/index-5PMZjYKR.css">
16
16
  </head>
17
17
  <body>
@@ -2,7 +2,7 @@ self.addEventListener('install', () => self.skipWaiting());
2
2
  self.addEventListener('activate', () => self.clients.claim());
3
3
  self.addEventListener('fetch', (e) => e.respondWith(fetch(e.request)));
4
4
 
5
- // Web Push del MCP bridge: payload JSON {title, body?, url?} dal server.
5
+ // Web Push del MCP bridge: payload JSON {title, body?, lang?, url?} dal server.
6
6
  // tag fisso 'nexuscrew': le notifiche si sostituiscono invece di accumularsi.
7
7
  self.addEventListener('push', (e) => {
8
8
  let data = {};
@@ -10,6 +10,7 @@ self.addEventListener('push', (e) => {
10
10
  const title = typeof data.title === 'string' && data.title ? data.title : 'NexusCrew';
11
11
  e.waitUntil(self.registration.showNotification(title, {
12
12
  body: typeof data.body === 'string' ? data.body : '',
13
+ ...(typeof data.lang === 'string' && data.lang ? { lang: data.lang } : {}),
13
14
  tag: 'nexuscrew',
14
15
  data: { url: typeof data.url === 'string' ? data.url : '/' },
15
16
  }));
@@ -1 +1 @@
1
- {"version":"0.8.37"}
1
+ {"version":"0.8.38"}
@@ -80,6 +80,7 @@ function filesRoutes({ cfg, sessionExists, paste, notifier, readonly = () => fal
80
80
  title: `file da ${session}`,
81
81
  body: b.caption ? `${saved.name} — ${b.caption}` : saved.name,
82
82
  session,
83
+ lang: 'it',
83
84
  }).catch(() => {});
84
85
  }
85
86
  res.json({ name: saved.name, box: 'outbox', size: saved.size });
package/lib/mcp/tools.js CHANGED
@@ -10,6 +10,7 @@
10
10
  // handler, identity gate e semantica read/write sono preservati EXACT.
11
11
  const path = require('node:path');
12
12
  const { codeOf, phaseOf } = require('../fleet/causes.js');
13
+ const { normalizeNotificationLang } = require('../notify/language.js');
13
14
  const {
14
15
  NODE_ID_RE, orderedDeckMembers, fleetStatusPath, fleetCellsBySession,
15
16
  routePath, topologyOwners, memberOwnerId, parseCellTarget, normalizeCellPayload,
@@ -158,6 +159,10 @@ const TOOLS = [
158
159
  title: { type: 'string', description: 'titolo breve della notifica' },
159
160
  body: { type: 'string', description: 'dettaglio opzionale' },
160
161
  urgency: { type: 'string', enum: ['normal', 'high'], description: 'default normal' },
162
+ lang: {
163
+ type: 'string',
164
+ description: 'lingua opzionale del testo: it, en, es o locale BCP-47 equivalente (es. it-IT)',
165
+ },
161
166
  },
162
167
  required: ['title'],
163
168
  },
@@ -165,11 +170,21 @@ const TOOLS = [
165
170
  const title = argString(args, 'title', { required: true, max: 200 });
166
171
  const body = argString(args, 'body', { max: 2000 });
167
172
  const urgency = argString(args, 'urgency', { max: 16 });
173
+ const rawLang = argString(args, 'lang', { max: 35 });
168
174
  if (urgency !== undefined && urgency !== 'normal' && urgency !== 'high') {
169
175
  throw new Error('urgency deve essere "normal" o "high"');
170
176
  }
177
+ const lang = rawLang === undefined ? undefined : normalizeNotificationLang(rawLang);
178
+ if (rawLang !== undefined && !lang) {
179
+ throw new Error('lang deve essere it, en, es o un locale BCP-47 con una di queste lingue base');
180
+ }
171
181
  const session = await ctx.session();
172
- const payload = { title, ...(body ? { body } : {}), ...(urgency ? { urgency } : {}) };
182
+ const payload = {
183
+ title,
184
+ ...(body ? { body } : {}),
185
+ ...(urgency ? { urgency } : {}),
186
+ ...(lang ? { lang } : {}),
187
+ };
173
188
  if (session) payload.session = session;
174
189
  const j = await ctx.api('POST', '/api/notify', payload);
175
190
  return { delivered: j.delivered };
@@ -0,0 +1,31 @@
1
+ 'use strict';
2
+
3
+ // Notification speech currently ships one stable browser locale per supported
4
+ // base language. The wire accepts ordinary BCP-47 subtags so callers can pass
5
+ // either `it` or `it-IT`, while keeping the public protocol deliberately small.
6
+ const SUPPORTED_NOTIFICATION_LANGS = new Set(['it', 'en', 'es']);
7
+ const NOTIFICATION_LANG_RE = /^(it|en|es)(?:-[a-z0-9]{2,8})*$/i;
8
+
9
+ function canonicalSubtag(part, index) {
10
+ if (index === 0) return part.toLowerCase();
11
+ if (/^[a-z]{2}$/i.test(part)) return part.toUpperCase();
12
+ if (/^[a-z]{4}$/i.test(part)) {
13
+ return `${part[0].toUpperCase()}${part.slice(1).toLowerCase()}`;
14
+ }
15
+ return part.toLowerCase();
16
+ }
17
+
18
+ function normalizeNotificationLang(value) {
19
+ if (typeof value !== 'string') return null;
20
+ const trimmed = value.trim();
21
+ if (!NOTIFICATION_LANG_RE.test(trimmed)) return null;
22
+ const parts = trimmed.split('-');
23
+ if (!SUPPORTED_NOTIFICATION_LANGS.has(parts[0].toLowerCase())) return null;
24
+ return parts.map(canonicalSubtag).join('-');
25
+ }
26
+
27
+ module.exports = {
28
+ NOTIFICATION_LANG_RE,
29
+ SUPPORTED_NOTIFICATION_LANGS,
30
+ normalizeNotificationLang,
31
+ };
@@ -4,7 +4,7 @@
4
4
  // consegna file in outbox — un solo punto che conosce entrambi i canali.
5
5
 
6
6
  function createNotifier({ hub, push }) {
7
- // frame: {title, body?, urgency?, session?, url?}. Ritorna {ui, push} (conteggi).
7
+ // frame: {title, body?, urgency?, session?, lang?, url?}. Ritorna {ui, push} (conteggi).
8
8
  async function emit(frame) {
9
9
  const ui = hub.broadcast({
10
10
  type: 'notify',
@@ -12,6 +12,7 @@ function createNotifier({ hub, push }) {
12
12
  ...(frame.body ? { body: String(frame.body) } : {}),
13
13
  urgency: frame.urgency === 'high' ? 'high' : 'normal',
14
14
  ...(frame.session ? { session: String(frame.session) } : {}),
15
+ ...(frame.lang ? { lang: String(frame.lang) } : {}),
15
16
  ts: Date.now(),
16
17
  });
17
18
  let pushed = 0;
@@ -19,6 +20,7 @@ function createNotifier({ hub, push }) {
19
20
  const r = await push.sendToAll({
20
21
  title: String(frame.title || ''),
21
22
  ...(frame.body ? { body: String(frame.body) } : {}),
23
+ ...(frame.lang ? { lang: String(frame.lang) } : {}),
22
24
  url: typeof frame.url === 'string' ? frame.url : '/',
23
25
  });
24
26
  pushed = r.sent;
@@ -19,8 +19,9 @@
19
19
  // niente Invio, control char rifiutati): qui si sanifica il testo PRIMA.
20
20
  const express = require('express');
21
21
  const { isValidSession } = require('../files/store.js');
22
+ const { normalizeNotificationLang } = require('./language.js');
22
23
 
23
- const NOTIFY_KEYS = new Set(['title', 'body', 'urgency', 'session']);
24
+ const NOTIFY_KEYS = new Set(['title', 'body', 'urgency', 'session', 'lang']);
24
25
  const ASK_KEYS = new Set(['question', 'options', 'session']);
25
26
  const RATE_MAX = 6;
26
27
  const RATE_WINDOW_MS = 60 * 1000;
@@ -110,7 +111,7 @@ function notifyRoutes({ cfg, notifier, push, asks, paste, sessionExists }) {
110
111
  return res.status(400).json({ error: 'body deve essere un oggetto JSON' });
111
112
  }
112
113
  for (const k of Object.keys(b)) {
113
- if (!NOTIFY_KEYS.has(k)) return res.status(400).json({ error: `chiave non ammessa: "${k}" (schema: title, body?, urgency?, session?)` });
114
+ if (!NOTIFY_KEYS.has(k)) return res.status(400).json({ error: `chiave non ammessa: "${k}" (schema: title, body?, urgency?, session?, lang?)` });
114
115
  }
115
116
  if (typeof b.title !== 'string' || !b.title.trim()) {
116
117
  return res.status(400).json({ error: 'title deve essere una stringa non vuota' });
@@ -122,6 +123,10 @@ function notifyRoutes({ cfg, notifier, push, asks, paste, sessionExists }) {
122
123
  if (b.urgency !== undefined && b.urgency !== 'normal' && b.urgency !== 'high') {
123
124
  return res.status(400).json({ error: 'urgency deve essere "normal" o "high"' });
124
125
  }
126
+ const lang = b.lang === undefined ? undefined : normalizeNotificationLang(b.lang);
127
+ if (b.lang !== undefined && !lang) {
128
+ return res.status(400).json({ error: 'lang deve essere it, en, es o un locale BCP-47 con una di queste lingue base' });
129
+ }
125
130
  if (b.session !== undefined && !isValidSession(b.session)) {
126
131
  return res.status(400).json({ error: 'session non valida' });
127
132
  }
@@ -130,7 +135,7 @@ function notifyRoutes({ cfg, notifier, push, asks, paste, sessionExists }) {
130
135
  return res.status(429).json({ error: 'rate limit notify superato (limite globale per token + per sessione)' });
131
136
  }
132
137
  const delivered = await notifier.emit({
133
- title: b.title.trim(), body: b.body, urgency: b.urgency, session: b.session,
138
+ title: b.title.trim(), body: b.body, urgency: b.urgency, session: b.session, lang,
134
139
  });
135
140
  res.json({ delivered });
136
141
  } catch (e) { res.status(500).json({ error: String(e.message || e) }); }
@@ -197,6 +202,7 @@ function notifyRoutes({ cfg, notifier, push, asks, paste, sessionExists }) {
197
202
  body: ask.question,
198
203
  urgency: 'high',
199
204
  session: ask.session,
205
+ lang: 'it',
200
206
  url: `/#ask=${ask.id}`,
201
207
  });
202
208
  res.status(201).json({ id: ask.id });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmmbuto/nexuscrew",
3
- "version": "0.8.37",
3
+ "version": "0.8.38",
4
4
  "description": "Faithful browser tmux client — attach to live sessions over a real PTY, localhost-only, mobile-easy",
5
5
  "main": "lib/server.js",
6
6
  "bin": {
@@ -24,7 +24,7 @@ When the client exposes the NexusCrew MCP server, use these tools directly:
24
24
 
25
25
  Apply these rules:
26
26
 
27
- - Use `nc_notify` for meaningful asynchronous updates, failures requiring attention, and completion. Do not notify for every command or duplicate routine chat commentary.
27
+ - Use `nc_notify` for meaningful asynchronous updates, failures requiring attention, and completion. Pass its optional `lang` field (`it`, `en`, `es` or an equivalent BCP-47 locale) whenever the notification text language is known, so opted-in speech uses the right on-device voice. Do not notify for every command or duplicate routine chat commentary.
28
28
  - Never include access tokens, credentials, private keys, push subscriptions, or other secrets in a notification, ask, file caption, or tool result.
29
29
  - Treat `nc_ask` as non-blocking: it returns an ask ID immediately. Continue safe independent work or wait normally; the human response arrives in the originating tmux session with a `[human reply · ask#<id>]` prefix by default.
30
30
  - Use `nc_status` instead of scraping NexusCrew state files. Use `nc_deck` instead of reading `decks.json`: it returns every local or authorized shared-owner deck containing the caller, preserves visual member order, identifies each deck and member by stable owner ID, includes viewer-valid Hydra routes, and reports `cell: null` when no managed Fleet match is available.