@chatpanel/events 0.101.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.
- package/adapters.js +1 -83
- package/adaptive-tool-policy.js +1 -45
- package/agent.js +2 -255
- package/attribution.js +1 -132
- package/backup-envelope.js +1 -221
- package/board-tool.js +2 -165
- package/budget.js +1 -116
- package/capability.js +1 -126
- package/citations.js +5 -127
- package/client-prefs.js +1 -148
- package/context-attachments.js +8 -144
- package/cowriter-router.js +1 -83
- package/cowriter-writer.js +19 -193
- package/cowriter.js +1 -190
- package/curate.js +3 -509
- package/distance.js +1 -124
- package/engine.js +1 -131
- package/entitlement.js +1 -332
- package/entity.js +1 -354
- package/event.js +1 -175
- package/extraction.js +4 -293
- package/failover.js +1 -93
- package/find-tool.js +1 -66
- package/flowchart.js +2 -644
- package/gate.js +1 -74
- package/harness.js +4 -101
- package/index.js +1 -345
- package/invariants.js +1 -174
- package/job.js +3 -147
- package/kernel.js +1 -255
- package/knowledge-derive.js +1 -267
- package/knowledge.js +3 -230
- package/library.js +11 -280
- package/loop.js +1 -132
- package/manifest.js +1 -107
- package/markdown-authoring.js +11 -288
- package/markdown-render.js +0 -0
- package/mcp-client.js +8 -358
- package/mcp-dispatch.js +1 -52
- package/mcp-errors.js +1 -106
- package/mcp-manager.js +1 -127
- package/media-transcript.js +8 -531
- package/meeting-analyzers.js +1 -83
- package/meeting-insights.js +7 -164
- package/meeting-shape.js +1 -282
- package/meeting-text.js +5 -94
- package/memory.js +2 -638
- package/model-candidates.js +1 -358
- package/model-health.js +1 -182
- package/model-ledger.js +1 -228
- package/model-picker.js +1 -188
- package/note-actions.js +15 -246
- package/note-graph.js +1 -158
- package/note-links.js +1 -68
- package/note-mentions.js +8 -114
- package/note-plan.js +9 -107
- package/note-research.js +1 -135
- package/observability.js +1 -119
- package/omni.js +1 -125
- package/order.js +1 -78
- package/package.json +1 -4
- package/pdf-layout.js +7 -253
- package/project-run.js +20 -444
- package/project.js +1 -170
- package/promotion.js +1 -171
- package/queue.js +1 -98
- package/reach.js +1 -23
- package/recipe-tool.js +2 -161
- package/recipe.js +1 -264
- package/record-list.js +2 -110
- package/recruit.js +3 -456
- package/redaction-tokens.js +1 -61
- package/ref.js +1 -55
- package/registry.js +1 -240
- package/route-graph.js +1 -115
- package/route-strategies.js +2 -232
- package/router.js +2 -880
- package/rrf.js +1 -89
- package/rules.js +1 -142
- package/schedule.js +1 -663
- package/scm-connection.js +1 -180
- package/scopes.js +1 -12
- package/scorecard.js +1 -335
- package/search-engines.js +1 -81
- package/skill-manifest.js +1 -330
- package/skill-scan.js +3 -209
- package/skill-sources.js +1 -162
- package/skill-vars.js +2 -252
- package/slash-commands.js +1 -158
- package/source-gate.js +1 -92
- package/sources-retrieval.js +5 -186
- package/sources.js +1 -273
- package/store.js +1 -171
- package/structured.js +4 -901
- package/subject-kinds.js +1 -5
- package/subject-name.js +1 -96
- package/sync-plan.js +1 -213
- package/synthesis.js +3 -123
- package/tags.js +1 -203
- package/team-board.js +6 -337
- package/team-cache.js +2 -63
- package/team-observe.js +1 -118
- package/team-org.js +1 -348
- package/team-plan.js +4 -131
- package/team-record.js +1 -225
- package/team-run.js +9 -652
- package/team-subtask.js +2 -191
- package/team-task.js +2 -124
- package/team-tool.js +2 -196
- package/team-trail.js +2 -73
- package/team-worklog.js +2 -149
- package/team.js +1 -302
- package/text-search.js +1 -155
- package/theme.js +17 -154
- package/titles.js +4 -285
- package/tool-discovery.js +1 -87
- package/tool-dispatch.js +5 -230
- package/tool-groups.js +1 -81
- package/tool-hints.js +10 -185
- package/tool-loop-guard.js +4 -182
- package/tool-need.js +2 -124
- package/tool-result.js +2 -438
- package/tool-round.js +0 -0
- package/tool-schema.js +1 -155
- package/tool-traits.js +1 -182
- package/toolset.js +2 -88
- package/trajectory.js +1 -530
- package/turn-loop.js +4 -440
- package/upcast.js +1 -37
- package/vault.js +2 -250
- package/view.js +1 -88
- package/voice-intents.js +2 -1488
- package/voice-speaker.js +1 -98
- package/weather-tool.js +1 -65
- package/weather.js +2 -211
- package/web-search-tool.js +14 -102
- package/web-search.js +1 -160
- package/widget.js +1 -153
package/web-search.js
CHANGED
|
@@ -1,160 +1 @@
|
|
|
1
|
-
|
|
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
|
-
|
|
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};
|