@chatpanel/events 0.102.0 → 0.103.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.
Files changed (138) hide show
  1. package/adapters.js +1 -83
  2. package/adaptive-tool-policy.js +1 -45
  3. package/agent.js +2 -255
  4. package/attribution.js +1 -132
  5. package/backup-envelope.js +1 -221
  6. package/board-tool.js +2 -165
  7. package/budget.js +1 -116
  8. package/capability.js +1 -126
  9. package/citations.js +5 -127
  10. package/client-prefs.js +1 -148
  11. package/context-attachments.js +8 -144
  12. package/cowriter-router.js +1 -83
  13. package/cowriter-writer.js +19 -193
  14. package/cowriter.js +1 -190
  15. package/curate.js +3 -509
  16. package/distance.js +1 -124
  17. package/engine.js +1 -131
  18. package/entitlement.js +1 -332
  19. package/entity.js +1 -354
  20. package/event.js +1 -175
  21. package/extraction.js +4 -293
  22. package/failover.js +1 -93
  23. package/find-tool.js +1 -66
  24. package/flowchart.js +2 -644
  25. package/gate.js +1 -74
  26. package/harness.js +4 -101
  27. package/index.js +1 -345
  28. package/invariants.js +1 -174
  29. package/job.js +3 -147
  30. package/kernel.js +1 -255
  31. package/knowledge-derive.js +1 -267
  32. package/knowledge.js +3 -230
  33. package/library.js +11 -280
  34. package/loop.js +1 -132
  35. package/manifest.js +1 -107
  36. package/markdown-authoring.js +11 -288
  37. package/markdown-render.js +0 -0
  38. package/mcp-client.js +8 -358
  39. package/mcp-dispatch.js +1 -52
  40. package/mcp-errors.js +1 -106
  41. package/mcp-manager.js +1 -127
  42. package/media-transcript.js +8 -531
  43. package/meeting-analyzers.js +1 -83
  44. package/meeting-insights.js +7 -164
  45. package/meeting-shape.js +1 -282
  46. package/meeting-text.js +5 -94
  47. package/memory.js +2 -638
  48. package/model-candidates.js +1 -358
  49. package/model-health.js +1 -182
  50. package/model-ledger.js +1 -228
  51. package/model-picker.js +1 -188
  52. package/note-actions.js +15 -246
  53. package/note-graph.js +1 -158
  54. package/note-links.js +1 -68
  55. package/note-mentions.js +8 -114
  56. package/note-plan.js +9 -107
  57. package/note-research.js +1 -135
  58. package/observability.js +1 -119
  59. package/omni.js +1 -125
  60. package/order.js +1 -78
  61. package/package.json +1 -4
  62. package/pdf-layout.js +7 -253
  63. package/project-run.js +20 -444
  64. package/project.js +1 -170
  65. package/promotion.js +1 -171
  66. package/queue.js +1 -98
  67. package/reach.js +1 -23
  68. package/recipe-tool.js +2 -161
  69. package/recipe.js +1 -264
  70. package/record-list.js +2 -110
  71. package/recruit.js +3 -456
  72. package/redaction-tokens.js +1 -61
  73. package/ref.js +1 -55
  74. package/registry.js +1 -240
  75. package/route-graph.js +1 -115
  76. package/route-strategies.js +2 -232
  77. package/router.js +2 -880
  78. package/rrf.js +1 -89
  79. package/rules.js +1 -142
  80. package/schedule.js +1 -663
  81. package/scm-connection.js +1 -180
  82. package/scopes.js +1 -12
  83. package/scorecard.js +1 -335
  84. package/search-engines.js +1 -81
  85. package/skill-manifest.js +1 -330
  86. package/skill-scan.js +3 -209
  87. package/skill-sources.js +1 -162
  88. package/skill-vars.js +2 -252
  89. package/slash-commands.js +1 -158
  90. package/source-gate.js +1 -92
  91. package/sources-retrieval.js +5 -186
  92. package/sources.js +1 -273
  93. package/store.js +1 -171
  94. package/structured.js +4 -901
  95. package/subject-kinds.js +1 -5
  96. package/subject-name.js +1 -96
  97. package/sync-plan.js +1 -213
  98. package/synthesis.js +3 -123
  99. package/tags.js +1 -203
  100. package/team-board.js +6 -337
  101. package/team-cache.js +2 -63
  102. package/team-observe.js +1 -118
  103. package/team-org.js +1 -442
  104. package/team-plan.js +4 -131
  105. package/team-record.js +1 -225
  106. package/team-run.js +9 -652
  107. package/team-subtask.js +2 -191
  108. package/team-task.js +2 -124
  109. package/team-tool.js +2 -196
  110. package/team-trail.js +2 -73
  111. package/team-worklog.js +2 -149
  112. package/team.js +1 -302
  113. package/text-search.js +1 -155
  114. package/theme.js +17 -154
  115. package/titles.js +4 -285
  116. package/tool-discovery.js +1 -87
  117. package/tool-dispatch.js +5 -230
  118. package/tool-groups.js +1 -81
  119. package/tool-hints.js +10 -185
  120. package/tool-loop-guard.js +4 -182
  121. package/tool-need.js +2 -124
  122. package/tool-result.js +2 -438
  123. package/tool-round.js +0 -0
  124. package/tool-schema.js +1 -155
  125. package/tool-traits.js +1 -182
  126. package/toolset.js +2 -88
  127. package/trajectory.js +1 -530
  128. package/turn-loop.js +4 -440
  129. package/upcast.js +1 -37
  130. package/vault.js +2 -250
  131. package/view.js +1 -88
  132. package/voice-intents.js +2 -1488
  133. package/voice-speaker.js +1 -98
  134. package/weather-tool.js +1 -65
  135. package/weather.js +2 -211
  136. package/web-search-tool.js +14 -102
  137. package/web-search.js +1 -160
  138. package/widget.js +1 -153
package/web-search.js CHANGED
@@ -1,160 +1 @@
1
- // Reading a search engine's results page: the rules, without the DOM.
2
- //
3
- // Scraping a SERP is mostly judgement, and the judgement is identical everywhere: which
4
- // engines to ask, how to build their URL safely from a template the user can edit, how to
5
- // unwrap the redirector every engine wraps its links in, and which of the hundred anchors on
6
- // the page are actually RESULTS rather than the engine's own navigation, promos and
7
- // newsletter sign-ups.
8
- //
9
- // None of that needs a document. What does need one is turning HTML into a list of anchors,
10
- // and that is the single thing each host injects: the extension has `DOMParser`, the desktop's
11
- // main process has neither and uses a small extractor, and a gateway that grew one could pass
12
- // its own. So there is ONE scraper with three front doors, rather than three scrapers.
13
- //
14
- // THE DANGEROUS PART IS THE URL, NOT THE HTML. A search template is user input that becomes a
15
- // fetch, so `buildSearchUrl` refuses anything that is not https and anything without a query
16
- // placeholder — and the caller is required to pass the host guard it already owns
17
- // (`assertFetchable`), because deciding whether an address is safe to fetch is not a question
18
- // this module is allowed to answer on its own.
19
-
20
- /**
21
- * The engines, in the order they are tried.
22
- *
23
- * `enabled` is what a fresh install starts with. Mojeek is absent rather than disabled:
24
- * tested, it returns nothing at all, and an engine that never answers is not a fallback — it
25
- * is latency plus a misleading "no results".
26
- */
27
- export const SEARCH_ENGINES = Object.freeze([
28
- Object.freeze({ id: 'startpage', name: 'Startpage', url: 'https://www.startpage.com/sp/search?query=%s', enabled: true }),
29
- Object.freeze({ id: 'duckduckgo', name: 'DuckDuckGo', url: 'https://html.duckduckgo.com/html/?q=%s', enabled: true }),
30
- Object.freeze({ id: 'google', name: 'Google', url: 'https://www.google.com/search?q=%s', enabled: false }),
31
- Object.freeze({ id: 'bing', name: 'Bing', url: 'https://www.bing.com/search?q=%s', enabled: false }),
32
- ]);
33
-
34
- /** How many results to take from one engine before moving on. */
35
- export const RESULTS_PER_ENGINE = 5;
36
-
37
- /**
38
- * Turn a template into a URL, refusing the ones that are not safe to fetch.
39
- *
40
- * `assertFetchable` is INJECTED rather than implemented here: the SSRF/private-host guard is
41
- * one shared primitive that the client, the gateway and the bridge all call, and a second
42
- * opinion about what counts as a private address is how the two drift apart. Omitting it is
43
- * allowed only where the caller has already checked — it throws loudly rather than silently
44
- * skipping, so "I forgot" and "I checked elsewhere" cannot look the same.
45
- */
46
- export function buildSearchUrl(template, query, { assertFetchable } = {}) {
47
- const t = String(template || '').trim();
48
- if (!/^https:\/\//i.test(t)) throw new Error('Search engine URL must start with https://');
49
- if (!t.includes('%s') && !t.includes('{q}')) {
50
- throw new Error('Search engine URL must contain a %s (or {q}) query placeholder');
51
- }
52
- const url = t.replace(/%s|\{q\}/g, encodeURIComponent(String(query || '').trim()));
53
- if (typeof assertFetchable === 'function') assertFetchable(url);
54
- return url;
55
- }
56
-
57
- /**
58
- * Unwrap the redirector an engine wraps its results in, so what is fetched and CITED is the
59
- * real destination rather than the engine's tracking URL.
60
- */
61
- export function unwrapRedirect(href) {
62
- try {
63
- const u = new URL(href, 'https://duckduckgo.com');
64
- const uddg = u.searchParams.get('uddg'); // DuckDuckGo /l/?uddg=<encoded>
65
- if (uddg) return decodeURIComponent(uddg);
66
- const other = u.searchParams.get('url') || u.searchParams.get('u');
67
- if (other && /^https?:/i.test(other)) return other;
68
- return u.href;
69
- } catch {
70
- return String(href || '');
71
- }
72
- }
73
-
74
- /**
75
- * Hosts that appear on a SERP and are never results: the engine's own family, and the promos
76
- * they carry (Startpage's StartMail, Mojeek's newsletter).
77
- *
78
- * Deliberately NOT a broad denylist — real results legitimately point at YouTube, Yahoo and
79
- * Google properties, and filtering those would be filtering the web.
80
- */
81
- const JUNK_HOSTS = /(^|\.)(startpage\.com|startmail\.com|startpage\.dev|mojeek\.com|buttondown\.(com|email)|ecosia\.org|duckduckgo\.com|search\.brave\.com|qwant\.com)$/i;
82
-
83
- /** Is this a link off the engine's own page, or part of the engine itself? */
84
- export function isResultHost(hostname, engineHost = '') {
85
- const host = String(hostname || '').toLowerCase();
86
- if (!host) return false;
87
- if (JUNK_HOSTS.test(host)) return false;
88
- const engineDomain = String(engineHost || '').split('.').slice(-2).join('.').toLowerCase();
89
- if (engineDomain && (host === engineDomain || host.endsWith(`.${engineDomain}`))) return false;
90
- return true;
91
- }
92
-
93
- /**
94
- * Turn a page's anchors into results.
95
- *
96
- * `anchors` is what the host's extractor produced: `{ href, text, snippet?, chrome? }`, where
97
- * `chrome` marks an anchor that sat inside a nav, footer or promo block. Everything after this
98
- * point is identical in every client, which is the reason the module exists.
99
- *
100
- * Deduped on origin+path rather than the full URL, so the same page offered twice with
101
- * different tracking parameters counts once.
102
- */
103
- export function pickResults(anchors, { engineHost = '', limit = RESULTS_PER_ENGINE, fallback = false } = {}) {
104
- const out = [];
105
- const seen = new Set();
106
- for (const a of anchors || []) {
107
- const href = unwrapRedirect(a?.href || '');
108
- let u;
109
- try { u = new URL(href); } catch { continue; }
110
- if (u.protocol !== 'https:' && u.protocol !== 'http:') continue;
111
- if (!isResultHost(u.hostname, engineHost)) continue;
112
- // The generic sweep sees the WHOLE page, so it is filtered harder: an anchor inside the
113
- // page's own chrome is navigation, not a result.
114
- if (fallback && a.chrome) continue;
115
- const title = String(a?.text || '').replace(/\s+/g, ' ').trim();
116
- // A link whose visible text is a word or two is an icon, a "next", or a breadcrumb.
117
- if (title.length < 6) continue;
118
- const key = u.origin + u.pathname;
119
- if (seen.has(key)) continue;
120
- seen.add(key);
121
- out.push({ url: href, title, snippet: String(a?.snippet || '').replace(/\s+/g, ' ').trim() });
122
- if (out.length >= limit) break;
123
- }
124
- return out;
125
- }
126
-
127
- /**
128
- * Merge what several engines returned into one list.
129
- *
130
- * Engine ORDER is preserved rather than interleaved: the first engine is the one the user put
131
- * first, and a result it ranked third is more likely to be wanted than the second engine's
132
- * first. Deduped across engines on origin+path, so two engines agreeing shows once.
133
- */
134
- export function mergeEngineResults(perEngine, limit = 8) {
135
- const out = [];
136
- const seen = new Set();
137
- for (const results of perEngine || []) {
138
- for (const r of results || []) {
139
- let key;
140
- try { const u = new URL(r.url); key = u.origin + u.pathname; } catch { key = r.url; }
141
- if (seen.has(key)) continue;
142
- seen.add(key);
143
- out.push(r);
144
- if (out.length >= limit) return out;
145
- }
146
- }
147
- return out;
148
- }
149
-
150
- /**
151
- * The engines to try, in order: the enabled ones first, then the rest.
152
- *
153
- * A search that found nothing should escalate to the engines the user configured but left off
154
- * rather than report failure — being wrong about which engine works today is much more likely
155
- * than the web having no answer.
156
- */
157
- export function engineOrder(engines = SEARCH_ENGINES) {
158
- const list = (Array.isArray(engines) ? engines : []).filter((e) => e && e.url && !e.retired);
159
- return [...list.filter((e) => e.enabled !== false), ...list.filter((e) => e.enabled === false)];
160
- }
1
+ const f=Object.freeze([Object.freeze({id:"startpage",name:"Startpage",url:"https://www.startpage.com/sp/search?query=%s",enabled:!0}),Object.freeze({id:"duckduckgo",name:"DuckDuckGo",url:"https://html.duckduckgo.com/html/?q=%s",enabled:!0}),Object.freeze({id:"google",name:"Google",url:"https://www.google.com/search?q=%s",enabled:!1}),Object.freeze({id:"bing",name:"Bing",url:"https://www.bing.com/search?q=%s",enabled:!1})]),h=5;function m(n,r,{assertFetchable:e}={}){const t=String(n||"").trim();if(!/^https:\/\//i.test(t))throw new Error("Search engine URL must start with https://");if(!t.includes("%s")&&!t.includes("{q}"))throw new Error("Search engine URL must contain a %s (or {q}) query placeholder");const s=t.replace(/%s|\{q\}/g,encodeURIComponent(String(r||"").trim()));return typeof e=="function"&&e(s),s}function p(n){try{const r=new URL(n,"https://duckduckgo.com"),e=r.searchParams.get("uddg");if(e)return decodeURIComponent(e);const t=r.searchParams.get("url")||r.searchParams.get("u");return t&&/^https?:/i.test(t)?t:r.href}catch{return String(n||"")}}const g=/(^|\.)(startpage\.com|startmail\.com|startpage\.dev|mojeek\.com|buttondown\.(com|email)|ecosia\.org|duckduckgo\.com|search\.brave\.com|qwant\.com)$/i;function d(n,r=""){const e=String(n||"").toLowerCase();if(!e||g.test(e))return!1;const t=String(r||"").split(".").slice(-2).join(".").toLowerCase();return!(t&&(e===t||e.endsWith(`.${t}`)))}function w(n,{engineHost:r="",limit:e=h,fallback:t=!1}={}){const s=[],c=new Set;for(const o of n||[]){const a=p(o?.href||"");let i;try{i=new URL(a)}catch{continue}if(i.protocol!=="https:"&&i.protocol!=="http:"||!d(i.hostname,r)||t&&o.chrome)continue;const u=String(o?.text||"").replace(/\s+/g," ").trim();if(u.length<6)continue;const l=i.origin+i.pathname;if(!c.has(l)&&(c.add(l),s.push({url:a,title:u,snippet:String(o?.snippet||"").replace(/\s+/g," ").trim()}),s.length>=e))break}return s}function S(n,r=8){const e=[],t=new Set;for(const s of n||[])for(const c of s||[]){let o;try{const a=new URL(c.url);o=a.origin+a.pathname}catch{o=c.url}if(!t.has(o)&&(t.add(o),e.push(c),e.length>=r))return e}return e}function b(n=f){const r=(Array.isArray(n)?n:[]).filter(e=>e&&e.url&&!e.retired);return[...r.filter(e=>e.enabled!==!1),...r.filter(e=>e.enabled===!1)]}export{h as RESULTS_PER_ENGINE,f as SEARCH_ENGINES,m as buildSearchUrl,b as engineOrder,d as isResultHost,S as mergeEngineResults,w as pickResults,p as unwrapRedirect};
package/widget.js CHANGED
@@ -1,153 +1 @@
1
- // WIDGETS small apps the user asks for and the model builds, which then live in THEIR
2
- // ChatPanel. A timer, a calculator, a sticky note, a unit converter, a habit tracker: things
3
- // nobody should have to file a feature request for. Two people's ChatPanel can differ
4
- // entirely, without either of them writing code and without us shipping a release.
5
- //
6
- // A widget is NOT a capability. A capability is reviewed and approved before it runs, so it
7
- // can be trusted to act. A widget is written by a model on a user's whim, so it is treated as
8
- // exactly what it is — untrusted code — and given the smallest surface that still makes it
9
- // useful:
10
- //
11
- // • its own state, and nothing else's. `state` is a private object keyed by widget id. A
12
- // timer remembers where it got to; a sticky note remembers its text. No widget can read
13
- // another's, and none can read the user's notes, meetings or chats.
14
- // • no network, no chrome.*, no DOM outside its sandbox — enforced by the host, not by
15
- // good behaviour here.
16
- //
17
- // Anything beyond that is a GRANT: a widget may REQUEST capabilities in its manifest, and
18
- // those do nothing until the user approves them. Requesting is not receiving — `grants` is
19
- // stored separately from the manifest precisely so a widget cannot edit its own permissions
20
- // by rewriting its own code.
21
- //
22
- // Pure and host-free: no DOM, no storage, no postMessage. The client owns the transport and
23
- // the persistence; this owns what is legal.
24
-
25
- import { EventError } from './event.js';
26
-
27
- const str = (v) => typeof v === 'string' && v.length > 0;
28
- const MAX_HTML = 512 * 1024; // a small app, not a bundled framework
29
- const MAX_STATE = 256 * 1024; // a note, a lap list — not a database
30
-
31
- export const WIDGET_SURFACES = Object.freeze(['panel', 'chat']);
32
-
33
- /**
34
- * Validate a widget MANIFEST — everything a user sees before deciding to keep it.
35
- */
36
- export function validateWidget(w) {
37
- if (!w || typeof w !== 'object') throw new EventError('SHAPE', 'widget must be an object');
38
- if (!str(w.id)) throw new EventError('SHAPE', 'widget.id required');
39
- if (!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(w.id)) {
40
- throw new EventError('SHAPE', 'widget.id must be lowercase [a-z0-9_-], max 64 chars');
41
- }
42
- if (!str(w.name)) throw new EventError('SHAPE', 'widget.name required');
43
- if (!str(w.html)) throw new EventError('SHAPE', 'widget.html required');
44
- if (w.html.length > MAX_HTML) throw new EventError('SHAPE', `widget.html exceeds ${MAX_HTML} bytes`);
45
- // An icon NAME (the client maps it to its own icon set), so a pinned widget is
46
- // recognisable at a glance rather than being one of five identical marks.
47
- if (w.icon != null && !(str(w.icon) && /^[a-z][a-z0-9-]{0,31}$/.test(w.icon))) {
48
- throw new EventError('SHAPE', 'widget.icon must be an icon name like "timer"');
49
- }
50
- if (w.surface != null && !WIDGET_SURFACES.includes(w.surface)) {
51
- throw new EventError('SHAPE', `widget.surface must be one of ${WIDGET_SURFACES}`);
52
- }
53
- if (w.height != null && !(Number.isInteger(w.height) && w.height > 0 && w.height <= 2000)) {
54
- throw new EventError('SHAPE', 'widget.height must be a positive integer <= 2000');
55
- }
56
- // Requesting is not receiving. This only records what the widget WANTS; the grant lives
57
- // outside the manifest so rewriting the widget can never widen its own permissions.
58
- if (w.requests != null && !(Array.isArray(w.requests) && w.requests.every(str))) {
59
- throw new EventError('SHAPE', 'widget.requests must be string[] (capability ids it asks for)');
60
- }
61
- return w;
62
- }
63
-
64
- /**
65
- * Validate a message a widget sent. Untrusted input: nothing is believed, and the widget's
66
- * own id is supplied by the HOST (which knows which frame sent it), never read from the
67
- * message — otherwise a widget could name someone else's id and read their state.
68
- */
69
- export function validateWidgetMessage(msg, { widgetId, grants = [] } = {}) {
70
- if (!str(widgetId)) throw new EventError('SHAPE', 'host must supply the sending widget id');
71
- if (!msg || typeof msg !== 'object') throw new EventError('SHAPE', 'widget message must be an object');
72
- if (!str(msg.callId)) throw new EventError('SHAPE', 'widget message needs a callId');
73
-
74
- switch (msg.op) {
75
- case 'state.get':
76
- return { op: 'state.get', widgetId, callId: msg.callId };
77
-
78
- case 'state.set': {
79
- if (msg.state === undefined) throw new EventError('SHAPE', 'state.set needs state');
80
- let size = 0;
81
- try { size = JSON.stringify(msg.state ?? null).length; }
82
- catch { throw new EventError('SHAPE', 'widget state must be JSON-serialisable'); }
83
- if (size > MAX_STATE) throw new EventError('SHAPE', `widget state exceeds ${MAX_STATE} bytes`);
84
- return { op: 'state.set', widgetId, callId: msg.callId, state: msg.state };
85
- }
86
-
87
- case 'invoke': {
88
- if (!str(msg.capability)) throw new EventError('SHAPE', 'invoke must name a capability');
89
- // THE REFUSAL THAT MATTERS. A widget gets what the user granted it and nothing else —
90
- // checked here, before any kernel guard has to have an opinion, and against the stored
91
- // grants rather than against anything the widget or its manifest claims.
92
- if (!grants.includes(msg.capability)) {
93
- throw new EventError('DENIED',
94
- `widget "${widgetId}" has no grant for "${msg.capability}" — the user must approve it first`);
95
- }
96
- if (msg.args != null && (typeof msg.args !== 'object' || Array.isArray(msg.args))) {
97
- throw new EventError('SHAPE', 'invoke args must be an object');
98
- }
99
- return { op: 'invoke', widgetId, callId: msg.callId, capability: msg.capability, args: msg.args || {} };
100
- }
101
-
102
- default:
103
- throw new EventError('SHAPE', `unknown widget op "${msg.op}"`);
104
- }
105
- }
106
-
107
- /**
108
- * The permissions a widget actually holds: the intersection of what it asked for and what the
109
- * user approved. Asking for more later cannot grant it — a request that was never approved
110
- * stays absent, so an updated widget re-asking silently gains nothing.
111
- */
112
- export function effectiveGrants(widget, approved = []) {
113
- const asked = new Set(widget?.requests || []);
114
- return (approved || []).filter((id) => asked.has(id));
115
- }
116
-
117
- // Pick an icon for a widget from what it is called. A pinned widget sits in a narrow strip
118
- // next to the others, so five identical marks are worse than no icon at all — the point of
119
- // pinning is to find the thing without reading.
120
- //
121
- // Returns an icon NAME, not a glyph: the client owns how it is drawn (the extension resolves
122
- // these through its vendored set, a mobile client through its own), and a name survives that
123
- // mapping in a way an emoji does not. Every name here exists in the extension's icon set.
124
- const ICON_WORDS = [
125
- [/\b(timers?|pomodoro|stopwatch|countdowns?|intervals?)\b/i, 'timer'],
126
- [/\b(calc|calculators?|math|arithmetic|tip)\b/i, 'hash'],
127
- [/\b(notes?|sticky|scratch|memos?|journals?)\b/i, 'notebook-pen'],
128
- [/\b(todos?|tasks?|checklists?|habits?|trackers?)\b/i, 'list-checks'],
129
- [/\b(charts?|graphs?|stats?|metrics?|dashboards?)\b/i, 'bar-chart-3'],
130
- [/\b(convert|converters?|units?|currency|exchange|weights?)\b/i, 'scale'],
131
- [/\b(calendars?|schedules?|agendas?|dates?)\b/i, 'calendar'],
132
- [/\b(clocks?|time|timezones?|zones?)\b/i, 'clock'],
133
- [/\b(goals?|targets?|focus|okrs?)\b/i, 'target'],
134
- [/\b(ideas?|brainstorm|prompts?)\b/i, 'lightbulb'],
135
- [/\b(dice|random|rolls?|roller|coins?|shuffle)\b/i, 'zap'],
136
- [/\b(search|find|lookup)\b/i, 'search'],
137
- [/\b(web|urls?|links?|browser)\b/i, 'globe'],
138
- [/\b(mood|mind|memory|brain)\b/i, 'brain'],
139
- [/\b(password|secrets?|vault|lock)\b/i, 'lock'],
140
- [/\b(quotes?|sayings?)\b/i, 'quote'],
141
- [/\b(meetings?|standups?|people|team)\b/i, 'users'],
142
- [/\b(music|player|sound|audio)\b/i, 'play'],
143
- [/\b(photos?|images?|gallery)\b/i, 'image'],
144
- [/\b(files?|documents?|docs?)\b/i, 'file-text'],
145
- ];
146
-
147
- /** The widget's own icon if it declared one, else one derived from its name. */
148
- export function widgetIcon(widget) {
149
- if (widget?.icon) return widget.icon;
150
- const name = String(widget?.name || '');
151
- for (const [re, iconName] of ICON_WORDS) if (re.test(name)) return iconName;
152
- return 'app-window'; // generic, but never the mark the shelf itself uses
153
- }
1
+ import{EventError as t}from"./event.js";const s=e=>typeof e=="string"&&e.length>0,o=512*1024,n=256*1024,c=Object.freeze(["panel","chat"]);function u(e){if(!e||typeof e!="object")throw new t("SHAPE","widget must be an object");if(!s(e.id))throw new t("SHAPE","widget.id required");if(!/^[a-z0-9][a-z0-9_-]{0,63}$/.test(e.id))throw new t("SHAPE","widget.id must be lowercase [a-z0-9_-], max 64 chars");if(!s(e.name))throw new t("SHAPE","widget.name required");if(!s(e.html))throw new t("SHAPE","widget.html required");if(e.html.length>o)throw new t("SHAPE",`widget.html exceeds ${o} bytes`);if(e.icon!=null&&!(s(e.icon)&&/^[a-z][a-z0-9-]{0,31}$/.test(e.icon)))throw new t("SHAPE",'widget.icon must be an icon name like "timer"');if(e.surface!=null&&!c.includes(e.surface))throw new t("SHAPE",`widget.surface must be one of ${c}`);if(e.height!=null&&!(Number.isInteger(e.height)&&e.height>0&&e.height<=2e3))throw new t("SHAPE","widget.height must be a positive integer <= 2000");if(e.requests!=null&&!(Array.isArray(e.requests)&&e.requests.every(s)))throw new t("SHAPE","widget.requests must be string[] (capability ids it asks for)");return e}function h(e,{widgetId:i,grants:a=[]}={}){if(!s(i))throw new t("SHAPE","host must supply the sending widget id");if(!e||typeof e!="object")throw new t("SHAPE","widget message must be an object");if(!s(e.callId))throw new t("SHAPE","widget message needs a callId");switch(e.op){case"state.get":return{op:"state.get",widgetId:i,callId:e.callId};case"state.set":{if(e.state===void 0)throw new t("SHAPE","state.set needs state");let r=0;try{r=JSON.stringify(e.state??null).length}catch{throw new t("SHAPE","widget state must be JSON-serialisable")}if(r>n)throw new t("SHAPE",`widget state exceeds ${n} bytes`);return{op:"state.set",widgetId:i,callId:e.callId,state:e.state}}case"invoke":{if(!s(e.capability))throw new t("SHAPE","invoke must name a capability");if(!a.includes(e.capability))throw new t("DENIED",`widget "${i}" has no grant for "${e.capability}" — the user must approve it first`);if(e.args!=null&&(typeof e.args!="object"||Array.isArray(e.args)))throw new t("SHAPE","invoke args must be an object");return{op:"invoke",widgetId:i,callId:e.callId,capability:e.capability,args:e.args||{}}}default:throw new t("SHAPE",`unknown widget op "${e.op}"`)}}function d(e,i=[]){const a=new Set(e?.requests||[]);return(i||[]).filter(r=>a.has(r))}const l=[[/\b(timers?|pomodoro|stopwatch|countdowns?|intervals?)\b/i,"timer"],[/\b(calc|calculators?|math|arithmetic|tip)\b/i,"hash"],[/\b(notes?|sticky|scratch|memos?|journals?)\b/i,"notebook-pen"],[/\b(todos?|tasks?|checklists?|habits?|trackers?)\b/i,"list-checks"],[/\b(charts?|graphs?|stats?|metrics?|dashboards?)\b/i,"bar-chart-3"],[/\b(convert|converters?|units?|currency|exchange|weights?)\b/i,"scale"],[/\b(calendars?|schedules?|agendas?|dates?)\b/i,"calendar"],[/\b(clocks?|time|timezones?|zones?)\b/i,"clock"],[/\b(goals?|targets?|focus|okrs?)\b/i,"target"],[/\b(ideas?|brainstorm|prompts?)\b/i,"lightbulb"],[/\b(dice|random|rolls?|roller|coins?|shuffle)\b/i,"zap"],[/\b(search|find|lookup)\b/i,"search"],[/\b(web|urls?|links?|browser)\b/i,"globe"],[/\b(mood|mind|memory|brain)\b/i,"brain"],[/\b(password|secrets?|vault|lock)\b/i,"lock"],[/\b(quotes?|sayings?)\b/i,"quote"],[/\b(meetings?|standups?|people|team)\b/i,"users"],[/\b(music|player|sound|audio)\b/i,"play"],[/\b(photos?|images?|gallery)\b/i,"image"],[/\b(files?|documents?|docs?)\b/i,"file-text"]];function f(e){if(e?.icon)return e.icon;const i=String(e?.name||"");for(const[a,r]of l)if(a.test(i))return r;return"app-window"}export{c as WIDGET_SURFACES,d as effectiveGrants,u as validateWidget,h as validateWidgetMessage,f as widgetIcon};