@chatpanel/events 0.31.0 → 0.33.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/index.js +4 -0
- package/package.json +1 -1
- package/voice-intents.js +67 -8
package/index.js
CHANGED
|
@@ -28,6 +28,10 @@ export { checkInvariants, INVARIANTS } from './invariants.js';
|
|
|
28
28
|
export { createMemoryAdapter, createLogStore, createBlobStore } from './store.js';
|
|
29
29
|
export { createRegistry, REGISTRY_STATES } from './registry.js';
|
|
30
30
|
export { defineSearchEngine, reconcileEngines, attemptOrder, ENGINE_KINDS, SearchEngineError } from './search-engines.js';
|
|
31
|
+
export {
|
|
32
|
+
getWeather, weatherUrl, parseWeather, formatWeather, areaLabel, isAmbiguousLocation,
|
|
33
|
+
WEATHER_HOST, WEATHER_TIMEOUT_MS, WeatherError,
|
|
34
|
+
} from './weather.js';
|
|
31
35
|
export { defineToolGroup, createToolGroupRegistry, ToolGroupError } from './tool-groups.js';
|
|
32
36
|
export { toolNeedFor } from './tool-need.js';
|
|
33
37
|
export { parseFlowchart, layoutFlowchart, renderFlowchartSvg } from './flowchart.js';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chatpanel/events",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.33.0",
|
|
4
4
|
"description": "The canonical ChatPanel event-log and capability contracts — typed durable facts, clock-free deterministic linearization, schema upcasting, and the invariants the replay harness asserts. Pure, dependency-free ESM shared by the ChatPanel extension, gateway and bridge.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/voice-intents.js
CHANGED
|
@@ -233,13 +233,35 @@ export function findWakeCommands(text, wake = compileWake(), { maxSentences = MA
|
|
|
233
233
|
const tokens = tokenize(raw);
|
|
234
234
|
if (!tokens.length) return [];
|
|
235
235
|
const hits = wakeHits(raw, tokens, wake);
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
236
|
+
const out = [];
|
|
237
|
+
// How far the last command emitted reaches. A wake phrase the speaker used as a WORD, inside
|
|
238
|
+
// a command already running, is part of that command and not a second one.
|
|
239
|
+
let covered = 0;
|
|
240
|
+
for (let idx = 0; idx < hits.length; idx++) {
|
|
241
|
+
const hit = hits[idx];
|
|
242
|
+
// THE PRODUCT'S OWN NAME IS A WORD PEOPLE SAY.
|
|
243
|
+
//
|
|
244
|
+
// "Okay chat panel, take the notes of whatever we spoke so far in chat panel notes" holds
|
|
245
|
+
// the wake phrase twice: once as an address, once as the name of where to put them. The
|
|
246
|
+
// second was treated as a fresh address, which did two things and both were wrong — it cut
|
|
247
|
+
// the command down to "…so far", losing where they wanted the notes, and it emitted
|
|
248
|
+
// "notes. So that is good." as a command of its own, which noteIntent matched. One spoken
|
|
249
|
+
// request became two notes and a truncated one.
|
|
250
|
+
//
|
|
251
|
+
// A mention that is not an address and falls inside the command already being carried is
|
|
252
|
+
// skipped. A mention that stands on its own still gets through — the intent match is the
|
|
253
|
+
// safety net for an address this heuristic misjudged, and dropping those outright would
|
|
254
|
+
// trade a stray action for a lost one.
|
|
255
|
+
if (!hit.addressed && hit.start < covered) continue;
|
|
256
|
+
// Bounded by the next ADDRESS, then by sentence count. Deliberately the next address and
|
|
257
|
+
// not the next mention: a second wake word is the end of the first command only when the
|
|
258
|
+
// speaker was turning to us again, and the sentence they are still saying is not that.
|
|
259
|
+
let stop = raw.length;
|
|
260
|
+
for (let j = idx + 1; j < hits.length; j++) if (hits[j].addressed) { stop = hits[j].start; break; }
|
|
240
261
|
const span = raw.slice(hit.end, stop);
|
|
241
262
|
const command = trimTrailingLeadIn(stripLeadIn(firstSentences(span, maxSentences)).trim());
|
|
242
|
-
|
|
263
|
+
covered = Math.max(covered, hit.end + span.indexOf(command) + command.length);
|
|
264
|
+
out.push({
|
|
243
265
|
command,
|
|
244
266
|
wake: hit.phrase,
|
|
245
267
|
heard: raw.slice(hit.start, hit.end),
|
|
@@ -248,8 +270,9 @@ export function findWakeCommands(text, wake = compileWake(), { maxSentences = MA
|
|
|
248
270
|
// What was said after the command's own sentences, up to the next address. Not part of
|
|
249
271
|
// the command — kept so a caller refining with a model has the surrounding words.
|
|
250
272
|
rest: raw.slice(hit.end + span.indexOf(command) + command.length, stop).trim(),
|
|
251
|
-
};
|
|
252
|
-
}
|
|
273
|
+
});
|
|
274
|
+
}
|
|
275
|
+
return out;
|
|
253
276
|
}
|
|
254
277
|
|
|
255
278
|
/**
|
|
@@ -380,7 +403,12 @@ export const REFINEMENT_SCHEMA = defineSchema({
|
|
|
380
403
|
name: { type: 'string', max: 48, describe: 'a label of at most 6 words' },
|
|
381
404
|
kind: {
|
|
382
405
|
type: 'enum',
|
|
383
|
-
|
|
406
|
+
// 'action' is here because it was MISSING, and the gap was silent. A browser command
|
|
407
|
+
// ("go to google.com and search for chat panel") is not something they want to KNOW, so
|
|
408
|
+
// it does not read as a question — and the only other bucket that fitted a rambling,
|
|
409
|
+
// narrated demo was "none", which is dropped without a word. Spoken four different ways
|
|
410
|
+
// in one meeting, it did nothing every time while the timer beside it worked.
|
|
411
|
+
values: ['question', 'action', 'monitor', 'note', 'skill', 'timer', 'none'],
|
|
384
412
|
// An unknown kind becomes a QUESTION — the least surprising thing to do with something
|
|
385
413
|
// someone asked for, and the only kind that is undone by ignoring the answer. Guessing
|
|
386
414
|
// "monitor" instead would leave a card watching the meeting that nobody asked for.
|
|
@@ -405,6 +433,7 @@ export function refinementPrompt(utterance) {
|
|
|
405
433
|
'',
|
|
406
434
|
'Pick the SMALLEST kind that does what they asked:',
|
|
407
435
|
' question — answer it once, now. The DEFAULT for anything they want to know.',
|
|
436
|
+
' action — DO something in the browser or an app ("go to google.com and search for X").',
|
|
408
437
|
' monitor — only if they asked to be told as the meeting CONTINUES ("let me know if",',
|
|
409
438
|
' "keep an eye on"). A one-off question is NOT a monitor.',
|
|
410
439
|
' note — they asked for notes written down ("take notes on", "write that up").',
|
|
@@ -474,6 +503,36 @@ export function settleRefinement(v) {
|
|
|
474
503
|
return { request, name, kind, skill: v.skill || '' };
|
|
475
504
|
}
|
|
476
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Did the user's own spoken words name this host?
|
|
508
|
+
*
|
|
509
|
+
* The authority test for a hands-free browser command. A URL a MODEL picked is
|
|
510
|
+
* attacker-influenced by construction — it has been reading page text and meeting captions —
|
|
511
|
+
* so it gets a confirmation dialog. A URL whose host the USER said out loud has already been
|
|
512
|
+
* reviewed by the only person that dialog would have asked, and a modal in a side panel is
|
|
513
|
+
* exactly what nobody in a meeting is looking at: "go to google.com and search for chat
|
|
514
|
+
* panel" was spoken four ways in one call and did nothing every time.
|
|
515
|
+
*
|
|
516
|
+
* Deliberately strict. The full hostname always counts ("google.com"). The bare first label
|
|
517
|
+
* counts ONLY for a two-label host, so saying "docs" can never authorise `docs.evil.test` —
|
|
518
|
+
* anything deeper has to be said in full.
|
|
519
|
+
*
|
|
520
|
+
* Shared rather than written in the panel because it is a pure decision with no platform in
|
|
521
|
+
* it: the bridge relays the same page tools, and a second copy of an authority rule is a
|
|
522
|
+
* second answer to "may this happen".
|
|
523
|
+
*/
|
|
524
|
+
export function spokenNamesHost(url, spoken) {
|
|
525
|
+
const said = String(spoken || '').toLowerCase();
|
|
526
|
+
if (!said) return false;
|
|
527
|
+
let host;
|
|
528
|
+
try { host = new URL(url).hostname.toLowerCase().replace(/^www\./, ''); } catch { return false; }
|
|
529
|
+
if (!host) return false;
|
|
530
|
+
if (said.includes(host)) return true;
|
|
531
|
+
const labels = host.split('.');
|
|
532
|
+
if (labels.length !== 2) return false;
|
|
533
|
+
return new RegExp(`\\b${labels[0].replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\b`).test(said);
|
|
534
|
+
}
|
|
535
|
+
|
|
477
536
|
/**
|
|
478
537
|
* The same answer, AS IT ARRIVES.
|
|
479
538
|
*
|