@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.
- package/CHANGELOG.md +25 -0
- package/frontend/dist/assets/{index-BU0QZe1s.js → index-0uO9YgxK.js} +33 -33
- package/frontend/dist/index.html +1 -1
- package/frontend/dist/sw.js +2 -1
- package/frontend/dist/version.json +1 -1
- package/lib/files/routes.js +1 -0
- package/lib/mcp/tools.js +16 -1
- package/lib/notify/language.js +31 -0
- package/lib/notify/notifier.js +3 -1
- package/lib/notify/routes.js +9 -3
- package/package.json +1 -1
- package/skills/nexuscrew-agent/SKILL.md +1 -1
package/frontend/dist/index.html
CHANGED
|
@@ -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-
|
|
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>
|
package/frontend/dist/sw.js
CHANGED
|
@@ -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.
|
|
1
|
+
{"version":"0.8.38"}
|
package/lib/files/routes.js
CHANGED
|
@@ -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 = {
|
|
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
|
+
};
|
package/lib/notify/notifier.js
CHANGED
|
@@ -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;
|
package/lib/notify/routes.js
CHANGED
|
@@ -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
|
@@ -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.
|