@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 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.31.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
- return hits.map((hit, idx) => {
237
- // Bounded by the NEXT address, then by sentence count. A second wake word is the end of
238
- // the first command however the sentences fall.
239
- const stop = idx + 1 < hits.length ? hits[idx + 1].start : raw.length;
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
- return {
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
- values: ['question', 'monitor', 'note', 'skill', 'timer', 'none'],
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
  *