@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/voice-speaker.js CHANGED
@@ -1,98 +1 @@
1
- // WHOSE VOICE IS THIS the gate that keeps a conversation to the person having it.
2
- //
3
- // In a voice conversation every finalized sentence is SENT, so anything the microphone hears
4
- // becomes a question: a television, a colleague at the next desk, someone answering their own
5
- // phone behind you. The engine transcribes them all perfectly and correctly, and the
6
- // assistant answers the room.
7
- //
8
- // The gateway already computes a 512-d speaker fingerprint per committed segment and clusters
9
- // it into a stable label for the session (`diarize-engine.js`). All that is missing is the
10
- // DECISION, which is this file: the first voice in a conversation is the person who started
11
- // it, and later sentences from a different voice are not their turn.
12
- //
13
- // THE FAILURE DIRECTION MATTERS MORE THAN THE FEATURE. An assistant that occasionally answers
14
- // the television is annoying; one that ignores YOU is broken, and from the outside the two
15
- // look identical — a mic that is open and going nowhere. So every uncertain case sends:
16
- //
17
- // · no speaker on the final (diarization off, model missing, embedding failed) → send;
18
- // · no primary enrolled yet → this is the first voice, enroll it and send;
19
- // · the primary has not been heard for a long time → adopt whoever is talking now and
20
- // send, because the phone may have been handed over, or the first voice may have been
21
- // the television while the user was drawing breath.
22
- //
23
- // Only one case holds: a DIFFERENT voice, while the person having the conversation is still
24
- // in it. That is the case the user asked for and the only one we can be confident about.
25
- //
26
- // The engine's own honesty note applies here too: embeddings separate different speakers
27
- // well, but similar voices can merge — so this gate can let a very similar voice through. It
28
- // never claims to be security, only to keep the room out of the conversation.
29
-
30
- /** How long the primary must be silent before another voice may take over the conversation. */
31
- export const RE_ENROLL_MS = 45_000;
32
-
33
- /** A label the gateway pins to the microphone channel; never a guess, so always the primary. */
34
- export const PINNED_SELF = 'You';
35
-
36
- const labelOf = (speaker) => {
37
- if (!speaker) return '';
38
- if (typeof speaker === 'string') return speaker;
39
- return String(speaker.label || speaker.id || '');
40
- };
41
-
42
- /**
43
- * A per-conversation speaker gate.
44
- *
45
- * @param {object} [opts]
46
- * @param {number} [opts.reEnrollMs] silence after which another voice may take over
47
- * @param {() => number} [opts.now] injected clock, so the rules are testable without waiting
48
- */
49
- export function createSpeakerGate({ reEnrollMs = RE_ENROLL_MS, now = Date.now } = {}) {
50
- let primary = ''; // the label of the person having this conversation
51
- let lastHeard = 0; // when the primary last said something
52
- let held = 0; // sentences kept out, for the UI to report honestly
53
-
54
- return {
55
- /** The enrolled voice, or '' before anyone has spoken. */
56
- primary: () => primary,
57
- /** How many sentences this gate has held back. */
58
- heldCount: () => held,
59
-
60
- /**
61
- * Should this finalized sentence become a turn?
62
- *
63
- * @param {{ speaker?: any }} [final]
64
- * @returns {{ send: boolean, reason: 'no-speaker'|'enrolled'|'primary'|'adopted'|'other', speaker: string }}
65
- */
66
- admit(final = {}) {
67
- const label = labelOf(final.speaker);
68
- // Diarization is optional and fails open — a sentence with no speaker is always sent.
69
- if (!label) return { send: true, reason: 'no-speaker', speaker: '' };
70
-
71
- const t = now();
72
- if (!primary) {
73
- primary = label;
74
- lastHeard = t;
75
- return { send: true, reason: 'enrolled', speaker: label };
76
- }
77
- if (label === primary) {
78
- lastHeard = t;
79
- return { send: true, reason: 'primary', speaker: label };
80
- }
81
- // A different voice. Only take over when the conversation has clearly moved on.
82
- if (t - lastHeard >= reEnrollMs) {
83
- primary = label;
84
- lastHeard = t;
85
- return { send: true, reason: 'adopted', speaker: label };
86
- }
87
- held += 1;
88
- return { send: false, reason: 'other', speaker: label };
89
- },
90
-
91
- /**
92
- * Forget who was talking. Called when a conversation starts, and by a user who wants the
93
- * gate to hear them again — a voice it merged or mislabelled must be recoverable without
94
- * ending the session.
95
- */
96
- reset() { primary = ''; lastHeard = 0; held = 0; },
97
- };
98
- }
1
+ const i=45e3,d="You",l=r=>r?typeof r=="string"?r:String(r.label||r.id||""):"";function p({reEnrollMs:r=45e3,now:s=Date.now}={}){let t="",n=0,o=0;return{primary:()=>t,heldCount:()=>o,admit(u={}){const e=l(u.speaker);if(!e)return{send:!0,reason:"no-speaker",speaker:""};const a=s();return t?e===t?(n=a,{send:!0,reason:"primary",speaker:e}):a-n>=r?(t=e,n=a,{send:!0,reason:"adopted",speaker:e}):(o+=1,{send:!1,reason:"other",speaker:e}):(t=e,n=a,{send:!0,reason:"enrolled",speaker:e})},reset(){t="",n=0,o=0}}}export{d as PINNED_SELF,i as RE_ENROLL_MS,p as createSpeakerGate};
package/weather-tool.js CHANGED
@@ -1,65 +1 @@
1
- // `weather` as a TOOL the spec, the guidance, and the answer shape, without the fetch.
2
- //
3
- // The extension answers "how is the weather in X" with this rather than with a web search,
4
- // because weather sites are rendered by scripts and a fetched page is navigation and no
5
- // temperature — which is exactly what a desktop turn without this tool produced: "I couldn't
6
- // retrieve reliable live weather". The engine (`weather.js`: the wttr.in query, the parse,
7
- // the ambiguity check) is already shared; this is the tool every client arms in front of it.
8
- //
9
- // `fetchJson(url, { timeoutMs })` is injected: each host owns its network guard and applies
10
- // it to a URL a MODEL supplied — attacker-influenced by construction.
11
-
12
- import { getWeather } from './weather.js';
13
-
14
- export const WEATHER_TOOL_NAME = 'weather';
15
-
16
- export const WEATHER_TOOL_SYSTEM =
17
- 'For weather, call `weather` FIRST — it answers the whole question in one request. Only '
18
- + 'fall back to web_search if it tells you to. Report the location it says it resolved, '
19
- + 'because a bare town name can geocode to the wrong place.';
20
-
21
- export function weatherToolProvider({ fetchJson } = {}) {
22
- if (typeof fetchJson !== 'function') throw new Error('weatherToolProvider: fetchJson required');
23
- return {
24
- specs: [
25
- {
26
- name: 'weather',
27
- // A read, declared: the round runner overlaps reads and serialises everything it
28
- // cannot classify, and "weather" is not a verb its name heuristic knows.
29
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
30
- description:
31
- 'Current conditions and a short forecast for one place, in a single request. Use this '
32
- + 'for any weather question instead of searching. Returns the location it actually '
33
- + 'resolved to — say which place the answer is for.',
34
- parameters: {
35
- type: 'object',
36
- properties: {
37
- location: {
38
- type: 'string',
39
- description: 'A place, as the user said it. Add a state or country only if THEY did '
40
- + '— "Fairview, OR" if they said so, plain "Fairview" if they did not.',
41
- },
42
- },
43
- required: ['location'],
44
- additionalProperties: false,
45
- },
46
- },
47
- ],
48
- system: WEATHER_TOOL_SYSTEM,
49
- async execute(name, input) {
50
- if (name !== 'weather') return JSON.stringify({ error: `Unknown tool: ${name}` });
51
- const location = String(input?.location || '').trim();
52
- if (!location) return 'No location provided to weather.';
53
- const got = await getWeather(location, { fetchJson });
54
- // THE FALLBACK IS AN INSTRUCTION, not an error string. A model handed "weather failed"
55
- // stops, or apologises; a model told which tool answers this next just uses it. The
56
- // whole point of preferring one source is that it must degrade to the general one.
57
- if (!got.ok) {
58
- return `The weather service could not answer for "${location}" (${got.reason}). `
59
- + `Now call web_search for "weather in ${location}" and answer from the results — `
60
- + 'do not tell the user a tool failed.';
61
- }
62
- return { text: got.text, note: 'ChatPanel · wttr.in' };
63
- },
64
- };
65
- }
1
+ import{getWeather as a}from"./weather.js";const l="weather",i="For weather, call `weather` FIRST it answers the whole question in one request. Only fall back to web_search if it tells you to. Report the location it says it resolved, because a bare town name can geocode to the wrong place.";function c({fetchJson:o}={}){if(typeof o!="function")throw new Error("weatherToolProvider: fetchJson required");return{specs:[{name:"weather",annotations:{readOnlyHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Current conditions and a short forecast for one place, in a single request. Use this for any weather question instead of searching. Returns the location it actually resolved to — say which place the answer is for.",parameters:{type:"object",properties:{location:{type:"string",description:'A place, as the user said it. Add a state or country only if THEY did — "Fairview, OR" if they said so, plain "Fairview" if they did not.'}},required:["location"],additionalProperties:!1}}],system:i,async execute(r,n){if(r!=="weather")return JSON.stringify({error:`Unknown tool: ${r}`});const e=String(n?.location||"").trim();if(!e)return"No location provided to weather.";const t=await a(e,{fetchJson:o});return t.ok?{text:t.text,note:"ChatPanel · wttr.in"}:`The weather service could not answer for "${e}" (${t.reason}). Now call web_search for "weather in ${e}" and answer from the results — do not tell the user a tool failed.`}}}export{l as WEATHER_TOOL_NAME,i as WEATHER_TOOL_SYSTEM,c as weatherToolProvider};
package/weather.js CHANGED
@@ -1,211 +1,2 @@
1
- // Weather one deterministic source, asked first, with search as the fallback.
2
- //
3
- // There was no weather capability at all, so every "how is the weather in X" went to
4
- // web_search and the model assembled an answer from whatever three sites it happened to
5
- // scrape. That is slow (several page fetches for one number), inconsistent between asks, and
6
- // it failed in the way scraping fails: a repeated search got refused by the loop guard, the
7
- // reworded retry returned nothing, and the model announced it had no access to weather.
8
- //
9
- // wttr.in answers the whole question in ONE request as JSON, which is why it is the default.
10
- // It is not authoritative and it is not always up, so the tool falls back to search rather
11
- // than pretending — but the common case costs one round trip and reads the same every time.
12
- //
13
- // THE GEOCODER IS THE KNOWN WEAKNESS, and it is handled by disclosure rather than cleverness.
14
- // "Issaquah" once resolved to somewhere obscure and the answer looked plausible and was for
15
- // the wrong place. wttr.in reports what it actually resolved to, so `formatWeather` always
16
- // prints that line: a wrong place is then visible instead of silent. Guessing a region on the
17
- // user's behalf would replace a visible error with an invisible one.
18
- //
19
- // Pure and platform-free, like every other module here: the fetch is INJECTED, so the
20
- // extension passes its SSRF-guarded secureFetch, the gateway passes its own, and neither has
21
- // a second copy of the parsing.
22
-
23
- export const WEATHER_HOST = 'https://wttr.in';
24
-
25
- /** Long enough for a cold CDN edge, short enough that a hung host still falls back. */
26
- export const WEATHER_TIMEOUT_MS = 7000;
27
-
28
- export class WeatherError extends Error {
29
- constructor(code, message) { super(message); this.name = 'WeatherError'; this.code = code; }
30
- }
31
-
32
- /**
33
- * The URL for one location.
34
- *
35
- * The location goes in the PATH, so it is percent-encoded whole — a bare `encodeURIComponent`
36
- * of "Seattle, WA" gives `Seattle%2C%20WA`, which wttr.in reads correctly, while leaving the
37
- * comma raw would let a location containing `/` or `?` reshape the request. Never interpolate
38
- * an unencoded user string into a path.
39
- */
40
- export function weatherUrl(location, { host = WEATHER_HOST } = {}) {
41
- const q = String(location ?? '').trim();
42
- if (!q) throw new WeatherError('NO_LOCATION', 'a location is required');
43
- if (q.length > 120) throw new WeatherError('BAD_LOCATION', 'location is too long to be one');
44
- return `${host}/${encodeURIComponent(q)}?format=j1`;
45
- }
46
-
47
- const num = (v) => {
48
- const n = Number(v);
49
- return Number.isFinite(n) ? n : null;
50
- };
51
- const str = (v) => String(v ?? '').trim();
52
- // wttr.in nests half its scalars as [{ value: "…" }], which is a shape, not data.
53
- const firstValue = (v) => (Array.isArray(v) ? str(v[0]?.value) : str(v));
54
-
55
- /**
56
- * wttr.in's `format=j1` body → a shape that does not mention wttr.in.
57
- *
58
- * Returns null for anything unusable rather than throwing: a malformed body and an outage are
59
- * the same event to the caller, and both mean "fall back to search".
60
- */
61
- export function parseWeather(json, { query = '' } = {}) {
62
- if (!json || typeof json !== 'object') return null;
63
- const cur = Array.isArray(json.current_condition) ? json.current_condition[0] : null;
64
- if (!cur) return null;
65
- const tempC = num(cur.temp_C);
66
- const tempF = num(cur.temp_F);
67
- // A reading with no temperature is not a weather reading. Guarded because an error page
68
- // served as JSON still parses into an object, and half-empty output read as real data is
69
- // worse than no data.
70
- if (tempC === null && tempF === null) return null;
71
-
72
- const areaRaw = Array.isArray(json.nearest_area) ? json.nearest_area[0] : null;
73
- const area = areaRaw ? {
74
- name: firstValue(areaRaw.areaName),
75
- region: firstValue(areaRaw.region),
76
- country: firstValue(areaRaw.country),
77
- lat: num(areaRaw.latitude),
78
- lon: num(areaRaw.longitude),
79
- } : null;
80
-
81
- const days = (Array.isArray(json.weather) ? json.weather : []).slice(0, 3).map((d) => ({
82
- date: str(d.date),
83
- maxC: num(d.maxtempC),
84
- maxF: num(d.maxtempF),
85
- minC: num(d.mintempC),
86
- minF: num(d.mintempF),
87
- sunrise: str(d.astronomy?.[0]?.sunrise),
88
- sunset: str(d.astronomy?.[0]?.sunset),
89
- // The midday slot is the day's headline condition; the array is 3-hourly.
90
- condition: firstValue(d.hourly?.[4]?.weatherDesc) || firstValue(d.hourly?.[0]?.weatherDesc),
91
- chanceOfRain: num(d.hourly?.[4]?.chanceofrain),
92
- }));
93
-
94
- return {
95
- query: str(query),
96
- area,
97
- now: {
98
- tempC,
99
- tempF,
100
- feelsLikeC: num(cur.FeelsLikeC),
101
- feelsLikeF: num(cur.FeelsLikeF),
102
- condition: firstValue(cur.weatherDesc),
103
- humidity: num(cur.humidity),
104
- windKph: num(cur.windspeedKmph),
105
- windMph: num(cur.windspeedMiles),
106
- windDir: str(cur.winddir16Point),
107
- precipMm: num(cur.precipMM),
108
- uv: num(cur.uvIndex),
109
- observedAt: str(cur.localObsDateTime),
110
- },
111
- days,
112
- };
113
- }
114
-
115
- /** "Seattle, Washington, United States" — whatever of it exists, deduped. */
116
- export function areaLabel(area, fallback = '') {
117
- if (!area) return fallback;
118
- const parts = [area.name, area.region, area.country].map(str).filter(Boolean);
119
- const seen = new Set();
120
- const uniq = parts.filter((p) => {
121
- const k = p.toLowerCase();
122
- if (seen.has(k)) return false;
123
- seen.add(k);
124
- return true;
125
- });
126
- return uniq.join(', ') || fallback;
127
- }
128
-
129
- /**
130
- * True when the user named a bare place with no region or country.
131
- *
132
- * Not used to REWRITE the query — that is how you turn a wrong answer into a confidently
133
- * wrong one. It is used to add a line telling the model to say which place it got, so the
134
- * user can correct it. Dozens of towns are called Fairview.
135
- */
136
- export function isAmbiguousLocation(location) {
137
- const q = str(location);
138
- if (!q) return false;
139
- return !/[,]/.test(q) && q.split(/\s+/).length <= 2;
140
- }
141
-
142
- const c2f = (c) => Math.round((c * 9) / 5 + 32);
143
- const temp = (c, f) => {
144
- const cc = c === null && f !== null ? Math.round(((f - 32) * 5) / 9) : c;
145
- const ff = f === null && c !== null ? c2f(c) : f;
146
- if (cc === null && ff === null) return '';
147
- if (cc === null) return `${ff}°F`;
148
- if (ff === null) return `${cc}°C`;
149
- return `${ff}°F / ${cc}°C`;
150
- };
151
-
152
- /** What the model reads. Compact — this is one fact, not a report. */
153
- export function formatWeather(w) {
154
- if (!w) return '';
155
- const where = areaLabel(w.area, w.query);
156
- const lines = [];
157
- // FIRST LINE NAMES THE PLACE IT ACTUALLY RESOLVED. The geocoder picks obscure matches for
158
- // bare town names, and an answer that does not say where it is from cannot be caught.
159
- lines.push(`Weather for ${where}${w.query && where.toLowerCase() !== w.query.toLowerCase() ? ` (asked: "${w.query}")` : ''}`);
160
- const n = w.now;
161
- const now = [`Now: ${temp(n.tempC, n.tempF)}`];
162
- if (n.feelsLikeC !== null || n.feelsLikeF !== null) now.push(`feels like ${temp(n.feelsLikeC, n.feelsLikeF)}`);
163
- if (n.condition) now.push(n.condition.toLowerCase());
164
- lines.push(now.join(', '));
165
- const detail = [];
166
- if (n.humidity !== null) detail.push(`humidity ${n.humidity}%`);
167
- if (n.windMph !== null || n.windKph !== null) {
168
- detail.push(`wind ${n.windMph !== null ? `${n.windMph} mph` : `${n.windKph} km/h`}${n.windDir ? ` ${n.windDir}` : ''}`);
169
- }
170
- if (n.precipMm !== null) detail.push(`precip ${n.precipMm} mm`);
171
- if (n.uv !== null) detail.push(`UV ${n.uv}`);
172
- if (detail.length) lines.push(detail.join(' · '));
173
- for (const d of w.days) {
174
- const range = `${temp(d.maxC, d.maxF)} high / ${temp(d.minC, d.minF)} low`;
175
- const rain = d.chanceOfRain !== null ? `, ${d.chanceOfRain}% rain` : '';
176
- lines.push(`${d.date}: ${range}${d.condition ? `, ${d.condition.toLowerCase()}` : ''}${rain}`);
177
- }
178
- if (n.observedAt) lines.push(`Observed ${n.observedAt} local. Source: wttr.in`);
179
- if (isAmbiguousLocation(w.query)) {
180
- lines.push('The place name was ambiguous — TELL THE USER which location this is for, and '
181
- + 'offer to re-check with a state or country if it is the wrong one.');
182
- }
183
- return lines.join('\n');
184
- }
185
-
186
- /**
187
- * Ask for one location's weather.
188
- *
189
- * @param fetchJson async (url, { timeoutMs }) -> parsed JSON. Injected, so the caller's
190
- * SSRF-guarded fetch is the only one that runs and this module stays runnable in Node,
191
- * a browser and a mobile runtime.
192
- * @returns { ok: true, weather, text } | { ok: false, reason }
193
- *
194
- * NEVER THROWS. Every failure is a reason the caller can hand to the model as "use search
195
- * instead", because an exception here would surface as a tool error and stop the turn on a
196
- * question the fallback can still answer.
197
- */
198
- export async function getWeather(location, { fetchJson, timeoutMs = WEATHER_TIMEOUT_MS, host = WEATHER_HOST } = {}) {
199
- let url;
200
- try { url = weatherUrl(location, { host }); } catch (e) { return { ok: false, reason: e.message }; }
201
- if (typeof fetchJson !== 'function') return { ok: false, reason: 'no fetch was provided' };
202
- let json;
203
- try {
204
- json = await fetchJson(url, { timeoutMs });
205
- } catch (e) {
206
- return { ok: false, reason: e?.message || 'the weather service did not answer' };
207
- }
208
- const weather = parseWeather(json, { query: location });
209
- if (!weather) return { ok: false, reason: 'the weather service returned nothing usable' };
210
- return { ok: true, weather, text: formatWeather(weather) };
211
- }
1
+ const f="https://wttr.in",L=7e3;class m extends Error{constructor(r,n){super(n),this.name="WeatherError",this.code=r}}function d(e,{host:r=f}={}){const n=String(e??"").trim();if(!n)throw new m("NO_LOCATION","a location is required");if(n.length>120)throw new m("BAD_LOCATION","location is too long to be one");return`${r}/${encodeURIComponent(n)}?format=j1`}const s=e=>{const r=Number(e);return Number.isFinite(r)?r:null},l=e=>String(e??"").trim(),h=e=>Array.isArray(e)?l(e[0]?.value):l(e);function w(e,{query:r=""}={}){if(!e||typeof e!="object")return null;const n=Array.isArray(e.current_condition)?e.current_condition[0]:null;if(!n)return null;const t=s(n.temp_C),a=s(n.temp_F);if(t===null&&a===null)return null;const i=Array.isArray(e.nearest_area)?e.nearest_area[0]:null,o=i?{name:h(i.areaName),region:h(i.region),country:h(i.country),lat:s(i.latitude),lon:s(i.longitude)}:null,c=(Array.isArray(e.weather)?e.weather:[]).slice(0,3).map(u=>({date:l(u.date),maxC:s(u.maxtempC),maxF:s(u.maxtempF),minC:s(u.mintempC),minF:s(u.mintempF),sunrise:l(u.astronomy?.[0]?.sunrise),sunset:l(u.astronomy?.[0]?.sunset),condition:h(u.hourly?.[4]?.weatherDesc)||h(u.hourly?.[0]?.weatherDesc),chanceOfRain:s(u.hourly?.[4]?.chanceofrain)}));return{query:l(r),area:o,now:{tempC:t,tempF:a,feelsLikeC:s(n.FeelsLikeC),feelsLikeF:s(n.FeelsLikeF),condition:h(n.weatherDesc),humidity:s(n.humidity),windKph:s(n.windspeedKmph),windMph:s(n.windspeedMiles),windDir:l(n.winddir16Point),precipMm:s(n.precipMM),uv:s(n.uvIndex),observedAt:l(n.localObsDateTime)},days:c}}function y(e,r=""){if(!e)return r;const n=[e.name,e.region,e.country].map(l).filter(Boolean),t=new Set;return n.filter(i=>{const o=i.toLowerCase();return t.has(o)?!1:(t.add(o),!0)}).join(", ")||r}function $(e){const r=l(e);return r?!/[,]/.test(r)&&r.split(/\s+/).length<=2:!1}const C=e=>Math.round(e*9/5+32),p=(e,r)=>{const n=e===null&&r!==null?Math.round((r-32)*5/9):e,t=r===null&&e!==null?C(e):r;return n===null&&t===null?"":n===null?`${t}°F`:t===null?`${n}°C`:`${t}°F / ${n}°C`};function g(e){if(!e)return"";const r=y(e.area,e.query),n=[];n.push(`Weather for ${r}${e.query&&r.toLowerCase()!==e.query.toLowerCase()?` (asked: "${e.query}")`:""}`);const t=e.now,a=[`Now: ${p(t.tempC,t.tempF)}`];(t.feelsLikeC!==null||t.feelsLikeF!==null)&&a.push(`feels like ${p(t.feelsLikeC,t.feelsLikeF)}`),t.condition&&a.push(t.condition.toLowerCase()),n.push(a.join(", "));const i=[];t.humidity!==null&&i.push(`humidity ${t.humidity}%`),(t.windMph!==null||t.windKph!==null)&&i.push(`wind ${t.windMph!==null?`${t.windMph} mph`:`${t.windKph} km/h`}${t.windDir?` ${t.windDir}`:""}`),t.precipMm!==null&&i.push(`precip ${t.precipMm} mm`),t.uv!==null&&i.push(`UV ${t.uv}`),i.length&&n.push(i.join(" · "));for(const o of e.days){const c=`${p(o.maxC,o.maxF)} high / ${p(o.minC,o.minF)} low`,u=o.chanceOfRain!==null?`, ${o.chanceOfRain}% rain`:"";n.push(`${o.date}: ${c}${o.condition?`, ${o.condition.toLowerCase()}`:""}${u}`)}return t.observedAt&&n.push(`Observed ${t.observedAt} local. Source: wttr.in`),$(e.query)&&n.push("The place name was ambiguous — TELL THE USER which location this is for, and offer to re-check with a state or country if it is the wrong one."),n.join(`
2
+ `)}async function x(e,{fetchJson:r,timeoutMs:n=7e3,host:t=f}={}){let a;try{a=d(e,{host:t})}catch(c){return{ok:!1,reason:c.message}}if(typeof r!="function")return{ok:!1,reason:"no fetch was provided"};let i;try{i=await r(a,{timeoutMs:n})}catch(c){return{ok:!1,reason:c?.message||"the weather service did not answer"}}const o=w(i,{query:e});return o?{ok:!0,weather:o,text:g(o)}:{ok:!1,reason:"the weather service returned nothing usable"}}export{f as WEATHER_HOST,L as WEATHER_TIMEOUT_MS,m as WeatherError,y as areaLabel,g as formatWeather,x as getWeather,$ as isAmbiguousLocation,w as parseWeather,d as weatherUrl};
@@ -1,107 +1,19 @@
1
- // `web_search` as a TOOL — the spec, the guidance, and how results are put in front of a
2
- // model, without the search itself.
3
- //
4
- // Running a search is platform work: the extension fetches SERPs from a service worker
5
- // with DOMParser, the desktop's main process reads anchors with a tokenizer, a gateway that
6
- // grew one would use its own fetch. What is identical everywhere is what the model is told
7
- // the tool does, what it is told about citing, and how a result list becomes text it can
8
- // cite from — so that is what lives here, and `search` is injected.
9
- //
10
- // The citation rules are in the RESULT, not only in the system prompt, on purpose: the
11
- // model reads them at the moment it has sources in hand, which is when it is most likely to
12
- // follow them, and a turn that never searches never pays for them.
1
+ const a="web_search",u='You can call web_search to look up current information from the web prices, news, recent events, documentation, or anything time-sensitive or that may have changed since your training. Call it whenever the user asks about such things instead of guessing or saying you are unsure. Cite results inline as markdown links — e.g. ([1](https://…)) — never HTML, <sup>, or bare numbers, and finish with a "Sources" list of the links you used.',c=Object.freeze({name:a,annotations:{readOnlyHint:!0,idempotentHint:!0,openWorldHint:!0},description:"Search the web and return ranked result snippets with their source URLs. Use this for current events, live prices/quotes, news, product/library docs, or any fact you are unsure about or that may have changed since training — prefer it over guessing.",parameters:{type:"object",properties:{query:{type:"string",description:"The search query — a few keywords work best."}},required:["query"],additionalProperties:!1}});function l(e){if(!e?.results?.length){const t=(e?.engines||[]).join(", ");return`No web results for "${e?.query||""}"`+(t?` (searched: ${t}).`:".")+" This may mean the engine blocked the request rather than that nothing exists — do NOT conclude the information is unavailable. Try a shorter, more general query (drop dates and qualifiers), and tell the user they can enable another search engine in ChatPanel settings if it keeps failing."}const r=Array.isArray(e.engines)?e.engines:[],o=e.results.map(t=>`[${t.rank}] [${t.title}](${t.url})`).join(`
2
+ `),s=e.results[0].url,n=`Web search results for "${e.query}" (engines: ${r.join(", ")}).
13
3
 
14
- export const WEB_SEARCH_TOOL_NAME = 'web_search';
4
+ Citation rules: when a claim draws on a result below, cite it inline as a markdown link to that result's URL — e.g. ([1](${s})). Cite multiple sources as separate links: ([1](url)) ([3](url)). Do NOT output HTML, <sup>, or bare bracket numbers like [1] — every citation must be a clickable markdown link. Finish with a "Sources" section that repeats, as markdown links, each source you cited.
15
5
 
16
- export const WEB_SEARCH_TOOL_SYSTEM =
17
- 'You can call web_search to look up current information from the web — prices, news, recent '
18
- + 'events, documentation, or anything time-sensitive or that may have changed since your training. '
19
- + 'Call it whenever the user asks about such things instead of guessing or saying you are unsure. '
20
- + 'Cite results inline as markdown links — e.g. ([1](https://…)) — never HTML, <sup>, or bare '
21
- + 'numbers, and finish with a "Sources" list of the links you used.';
6
+ Sources:
7
+ ${o}`,i=e.results.map(t=>`### [${t.rank}] ${t.title}
8
+ <${t.url}>
22
9
 
23
- export const WEB_SEARCH_SPEC = Object.freeze({
24
- name: WEB_SEARCH_TOOL_NAME,
25
- annotations: { readOnlyHint: true, idempotentHint: true, openWorldHint: true },
26
- description:
27
- 'Search the web and return ranked result snippets with their source URLs. Use this for '
28
- + 'current events, live prices/quotes, news, product/library docs, or any fact you are unsure '
29
- + 'about or that may have changed since training — prefer it over guessing.',
30
- parameters: {
31
- type: 'object',
32
- properties: {
33
- query: { type: 'string', description: 'The search query — a few keywords work best.' },
34
- },
35
- required: ['query'],
36
- additionalProperties: false,
37
- },
38
- });
10
+ ${t.text}`).join(`
39
11
 
40
- /**
41
- * Flatten a search result into one readable blob the model can cite from.
42
- *
43
- * `res` is `{ query, engines: [ids], results: [{ rank, title, url, text }] }`. A citation
44
- * index of markdown links sits at the TOP so it survives truncation.
45
- */
46
- export function searchResultsToText(res) {
47
- if (!res?.results?.length) {
48
- // Name the engines HERE especially. The success path already lists them; the failure
49
- // path did not, so "no web results" looked like "the web has nothing" when it usually
50
- // means "the one engine we were allowed to ask returned nothing" — a search engine
51
- // blocking us and a query with no answer are completely different problems, and the
52
- // model cannot tell them apart without this.
53
- const tried = (res?.engines || []).join(', ');
54
- return `No web results for "${res?.query || ''}"`
55
- + (tried ? ` (searched: ${tried}).` : '.')
56
- + ' This may mean the engine blocked the request rather than that nothing exists —'
57
- + ' do NOT conclude the information is unavailable. Try a shorter, more general query'
58
- + ' (drop dates and qualifiers), and tell the user they can enable another search'
59
- + ' engine in ChatPanel settings if it keeps failing.';
60
- }
61
- const engines = Array.isArray(res.engines) ? res.engines : [];
62
- const sources = res.results.map((r) => `[${r.rank}] [${r.title}](${r.url})`).join('\n');
63
- const example = res.results[0].url;
64
- const head =
65
- `Web search results for "${res.query}" (engines: ${engines.join(', ')}).\n\n`
66
- + 'Citation rules: when a claim draws on a result below, cite it inline as a markdown '
67
- + `link to that result's URL — e.g. ([1](${example})). Cite multiple sources as separate `
68
- + 'links: ([1](url)) ([3](url)). Do NOT output HTML, <sup>, or bare bracket numbers like '
69
- + '[1] — every citation must be a clickable markdown link. Finish with a "Sources" section '
70
- + 'that repeats, as markdown links, each source you cited.\n\n'
71
- + `Sources:\n${sources}`;
72
- const body = res.results
73
- .map((r) => `### [${r.rank}] ${r.title}\n<${r.url}>\n\n${r.text}`)
74
- .join('\n\n---\n\n');
75
- return `${head}\n\n---\nResult details:\n\n${body}`;
76
- }
12
+ ---
77
13
 
78
- /**
79
- * The tool provider — `{ specs, system, execute }` for `buildToolset`.
80
- *
81
- * @param search `(query) => Promise<{ query, engines, results }>` — the host's search. It
82
- * may throw; the model gets the message rather than a dead turn.
83
- */
84
- export function webSearchToolProvider({ search } = {}) {
85
- if (typeof search !== 'function') throw new Error('webSearchToolProvider: search required');
86
- return {
87
- specs: [WEB_SEARCH_SPEC],
88
- system: WEB_SEARCH_TOOL_SYSTEM,
89
- async execute(name, input) {
90
- if (name !== WEB_SEARCH_TOOL_NAME) return JSON.stringify({ error: `Unknown tool: ${name}` });
91
- const q = String(input?.query || '').trim();
92
- if (!q) return 'No query provided to web_search.';
93
- try {
94
- const res = await search(q);
95
- // Return an OBJECT so the step can name the ENGINE that actually served the results.
96
- // "web_search" alone doesn't tell the user whether Startpage or DuckDuckGo answered —
97
- // which matters, because engines differ in coverage, and because a CLI agent may have
98
- // run its OWN search instead of this one. `note` becomes the step's badge; `text` is
99
- // what the model reads, unchanged.
100
- const engines = (res?.engines || []).join(', ');
101
- return { text: searchResultsToText(res), note: engines ? `ChatPanel · ${engines}` : 'ChatPanel' };
102
- } catch (e) {
103
- return `web_search failed: ${e?.message || e}`;
104
- }
105
- },
106
- };
107
- }
14
+ `);return`${n}
15
+
16
+ ---
17
+ Result details:
18
+
19
+ ${i}`}function h({search:e}={}){if(typeof e!="function")throw new Error("webSearchToolProvider: search required");return{specs:[c],system:u,async execute(r,o){if(r!==a)return JSON.stringify({error:`Unknown tool: ${r}`});const s=String(o?.query||"").trim();if(!s)return"No query provided to web_search.";try{const n=await e(s),i=(n?.engines||[]).join(", ");return{text:l(n),note:i?`ChatPanel · ${i}`:"ChatPanel"}}catch(n){return`web_search failed: ${n?.message||n}`}}}}export{c as WEB_SEARCH_SPEC,a as WEB_SEARCH_TOOL_NAME,u as WEB_SEARCH_TOOL_SYSTEM,l as searchResultsToText,h as webSearchToolProvider};