@chatpanel/events 0.102.0 → 0.104.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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 +4 -5
- package/pdf-layout.js +7 -253
- package/policy.js +1 -0
- 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 -442
- 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/tool-traits.js
CHANGED
|
@@ -1,182 +1 @@
|
|
|
1
|
-
|
|
2
|
-
// turned into the three decisions every host was making by hand.
|
|
3
|
-
//
|
|
4
|
-
// MCP tools carry `annotations` (readOnlyHint, destructiveHint, idempotentHint,
|
|
5
|
-
// openWorldHint) and nothing in any ChatPanel client read them. Meanwhile each client
|
|
6
|
-
// decided separately whether a call could run alongside another (never — every round ran
|
|
7
|
-
// one call at a time), whether a repeat could be answered from the last result (a
|
|
8
|
-
// hand-kept list of seven tool names), and whether an action deserved a confirmation
|
|
9
|
-
// (per-action, per-site rules with no idea what the tool itself declared). Three
|
|
10
|
-
// decisions, one fact underneath: is this a read, and could it destroy something.
|
|
11
|
-
//
|
|
12
|
-
// Annotations win when present. When absent, the NAME is read — `get_`, `list_`,
|
|
13
|
-
// `search_` on one side, `delete_`, `purge_`, `revoke_` on the other — with the
|
|
14
|
-
// conservative default in the middle: a tool nobody can classify is a write, not a read,
|
|
15
|
-
// and "unknown" is never promoted to "safe".
|
|
16
|
-
//
|
|
17
|
-
// Class R: strings in, booleans out. No I/O, so the extension, desktop, gateway and bridge
|
|
18
|
-
// give the same answer for the same spec.
|
|
19
|
-
|
|
20
|
-
const READ_VERBS = new Set([
|
|
21
|
-
'get', 'list', 'search', 'read', 'find', 'fetch', 'query', 'describe', 'lookup', 'look', 'show',
|
|
22
|
-
'view', 'count', 'check', 'status', 'recall', 'inspect', 'screenshot', 'preview', 'browse', 'grep',
|
|
23
|
-
'glob', 'head', 'tail', 'cat', 'ls', 'stat', 'resolve', 'detect', 'validate', 'explain', 'summarize',
|
|
24
|
-
'summarise', 'summary', 'history', 'recent', 'latest', 'whoami', 'info', 'render', 'compare', 'diff',
|
|
25
|
-
'ping', 'health', 'select', 'retrieve', 'load', 'peek', 'watch', 'tools', 'schema', 'suggest', 'smart',
|
|
26
|
-
]);
|
|
27
|
-
|
|
28
|
-
// Anything that changes the world without necessarily destroying part of it. A name that
|
|
29
|
-
// carries one of these is never a read, whatever else it carries: `search_and_replace`
|
|
30
|
-
// searches, and replaces.
|
|
31
|
-
const WRITE_VERBS = new Set([
|
|
32
|
-
'create', 'update', 'set', 'post', 'put', 'send', 'write', 'add', 'edit', 'move', 'rename',
|
|
33
|
-
'upload', 'run', 'execute', 'exec', 'click', 'type', 'submit', 'save', 'insert', 'patch', 'apply',
|
|
34
|
-
'merge', 'push', 'commit', 'start', 'stop', 'enable', 'disable', 'assign', 'close', 'reopen',
|
|
35
|
-
'login', 'logout', 'install', 'replace', 'transfer', 'pay', 'order', 'book', 'schedule',
|
|
36
|
-
'publish', 'deploy', 'restart', 'import', 'sync', 'remember', 'label', 'tag', 'mark', 'fill',
|
|
37
|
-
'press', 'scroll', 'navigate', 'open', 'goto', 'drag', 'invoke', 'call', 'trigger', 'act',
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
const DESTRUCTIVE_VERBS = new Set([
|
|
41
|
-
'delete', 'remove', 'drop', 'purge', 'destroy', 'reset', 'clear', 'kill', 'wipe', 'revoke',
|
|
42
|
-
'truncate', 'erase', 'overwrite', 'rm', 'rmdir', 'uninstall', 'terminate', 'ban', 'force',
|
|
43
|
-
'unlink', 'discard', 'forget',
|
|
44
|
-
]);
|
|
45
|
-
|
|
46
|
-
// `mcp_<server>__<tool>` → `<tool>`; `jira:delete-issue` → `delete-issue`.
|
|
47
|
-
export function bareToolName(name) {
|
|
48
|
-
const s = String(name || '');
|
|
49
|
-
const i = s.lastIndexOf('__');
|
|
50
|
-
return i >= 0 ? s.slice(i + 2) : s.replace(/^mcp[_-]/i, '');
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const tokensOf = (name) => bareToolName(name).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
|
|
54
|
-
|
|
55
|
-
function hasHint(a) {
|
|
56
|
-
return !!a && typeof a === 'object' && ['readOnlyHint', 'destructiveHint', 'idempotentHint', 'openWorldHint'].some((k) => typeof a[k] === 'boolean');
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* @returns {{ readOnly: boolean, destructive: boolean, idempotent: boolean, openWorld: boolean, source: 'annotations'|'heuristic' }}
|
|
61
|
-
*/
|
|
62
|
-
export function toolTraits(spec) {
|
|
63
|
-
const name = typeof spec === 'string' ? spec : spec?.name;
|
|
64
|
-
const a = typeof spec === 'object' && spec ? spec.annotations : null;
|
|
65
|
-
const toks = tokensOf(name);
|
|
66
|
-
const namedDestructive = toks.some((t) => DESTRUCTIVE_VERBS.has(t));
|
|
67
|
-
const namedWrite = namedDestructive || toks.some((t) => WRITE_VERBS.has(t));
|
|
68
|
-
// A read verb anywhere (`web_search`, `issue_get`) — as long as nothing in the name writes.
|
|
69
|
-
const namedRead = !namedWrite && toks.some((t) => READ_VERBS.has(t));
|
|
70
|
-
|
|
71
|
-
if (hasHint(a)) {
|
|
72
|
-
const readOnly = a.readOnlyHint === true;
|
|
73
|
-
// The spec's default for an absent destructiveHint is TRUE, which would put every
|
|
74
|
-
// `create_issue` behind a confirmation. Explicit wins; absent falls back to the name.
|
|
75
|
-
const destructive = readOnly ? false : (typeof a.destructiveHint === 'boolean' ? a.destructiveHint : namedDestructive);
|
|
76
|
-
return {
|
|
77
|
-
readOnly,
|
|
78
|
-
destructive,
|
|
79
|
-
idempotent: readOnly || a.idempotentHint === true,
|
|
80
|
-
openWorld: typeof a.openWorldHint === 'boolean' ? a.openWorldHint : true,
|
|
81
|
-
source: 'annotations',
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
return { readOnly: namedRead, destructive: namedDestructive, idempotent: namedRead, openWorld: true, source: 'heuristic' };
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Two reads never race each other for the world; a write is a barrier. */
|
|
88
|
-
export const canRunConcurrently = (t) => !!t?.readOnly;
|
|
89
|
-
|
|
90
|
-
/** A read asked twice has one answer, so the second can be served from the first. */
|
|
91
|
-
export const isCacheable = (t) => !!t?.readOnly;
|
|
92
|
-
|
|
93
|
-
/** Destroying is the one thing a person should be asked about, whatever the site rules say. */
|
|
94
|
-
export const needsConfirmation = (t) => t?.destructive === true;
|
|
95
|
-
|
|
96
|
-
/** Name → traits for a whole toolset, including the tools a dispatcher hides. */
|
|
97
|
-
export function traitsIndex(specs = []) {
|
|
98
|
-
const index = new Map();
|
|
99
|
-
for (const s of specs) if (s?.name) index.set(s.name, toolTraits(s));
|
|
100
|
-
return index;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Ask before a destructive call — the gate, as a toolset wrapper with the dialog injected.
|
|
105
|
-
*
|
|
106
|
-
* Page actions have had a confirmation card since the beginning; a tool on a connected
|
|
107
|
-
* server that DECLARES itself destructive (`destructiveHint`, or a name like `delete_repo`)
|
|
108
|
-
* had none: the model called it and it ran. This asks first, on the REAL tool name even
|
|
109
|
-
* when a dispatcher hides it, and refuses when there is nobody to ask — a background turn
|
|
110
|
-
* with no window cannot destroy anything a person did not approve.
|
|
111
|
-
*
|
|
112
|
-
* @param confirm async ({ name, input, via }) => 'allow' | 'always' | 'deny'. `always`
|
|
113
|
-
* allows this tool for the rest of the toolset's life (one session).
|
|
114
|
-
* Omit it on a surface with no window: destructive calls are then refused
|
|
115
|
-
* with a message the model can act on.
|
|
116
|
-
* @param only `(name) => boolean` — which top-level tools the gate covers (a host that
|
|
117
|
-
* already confirms its page and note tools passes its remote set).
|
|
118
|
-
* @param traitsOf `(name, input) => traits`; defaults to the toolset's index, then the name.
|
|
119
|
-
*/
|
|
120
|
-
export function withDestructiveGate(toolset, { confirm = null, only = () => true, traitsOf = null, effectiveName = defaultEffectiveName } = {}) {
|
|
121
|
-
if (!toolset || typeof toolset.execute !== 'function') return toolset;
|
|
122
|
-
const base = toolset.execute.bind(toolset);
|
|
123
|
-
const byName = new Map((toolset.specs || []).filter((s) => s?.name).map((s) => [s.name, s]));
|
|
124
|
-
const allowed = new Set();
|
|
125
|
-
const traits = traitsOf || ((name, input) => {
|
|
126
|
-
const eff = effectiveName(name, input);
|
|
127
|
-
return toolset.traits?.get(eff) || toolset.traits?.get(name) || toolTraits(byName.get(eff) || byName.get(name) || { name: eff });
|
|
128
|
-
});
|
|
129
|
-
return {
|
|
130
|
-
...toolset,
|
|
131
|
-
async execute(name, input, meta) {
|
|
132
|
-
if (!only(name)) return base(name, input, meta);
|
|
133
|
-
const t = traits(name, input);
|
|
134
|
-
if (!needsConfirmation(t)) return base(name, input, meta);
|
|
135
|
-
const eff = effectiveName(name, input);
|
|
136
|
-
if (allowed.has(eff)) return base(name, input, meta);
|
|
137
|
-
if (!confirm) {
|
|
138
|
-
return JSON.stringify({
|
|
139
|
-
error: `"${eff}" is a destructive action and needs the user's confirmation, which this surface cannot ask for. Do not retry it; tell the user what you would do and let them run it from the side panel.`,
|
|
140
|
-
blocked: true, needsConfirmation: true, tool: eff,
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
const decision = await confirm({ name: eff, input, via: name === eff ? null : name, traits: t });
|
|
144
|
-
if (decision === 'always') allowed.add(eff);
|
|
145
|
-
if (decision !== 'allow' && decision !== 'always') {
|
|
146
|
-
return JSON.stringify({ error: `The user DECLINED "${eff}". Do not retry it — stop and ask the user how to proceed.`, blocked: true, declined: true, tool: eff });
|
|
147
|
-
}
|
|
148
|
-
return base(name, input, meta);
|
|
149
|
-
},
|
|
150
|
-
};
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
// A dispatcher carries the real action in `input.action`; every name-based policy must
|
|
154
|
-
// see through it or `mcp {action:"mcp_x__delete_repo"}` is judged by the name "mcp" — and
|
|
155
|
-
// `page {action:'screenshot'}` taken four times looks like a stuck loop instead of a look.
|
|
156
|
-
// One definition: the extension, the desktop and the gate each had their own copy.
|
|
157
|
-
export function effectiveToolName(name, input) {
|
|
158
|
-
const action = input && typeof input === 'object' ? input.action : null;
|
|
159
|
-
return typeof action === 'string' && action ? action : name;
|
|
160
|
-
}
|
|
161
|
-
const defaultEffectiveName = effectiveToolName;
|
|
162
|
-
|
|
163
|
-
// Local tools whose reads may overlap in one round: they touch the user's own data or the
|
|
164
|
-
// network, never the one tab a page tool is driving. Everything not remote and not here
|
|
165
|
-
// runs one at a time, whatever its name says — a wrong "parallel" races the world, a wrong
|
|
166
|
-
// "serial" only costs latency. The `find` dispatcher is here as a whole: everything behind
|
|
167
|
-
// it is a read of the user's data or the web (its writes are separate tools by design).
|
|
168
|
-
//
|
|
169
|
-
// ONE list. The extension and the desktop each kept their own and they drifted within
|
|
170
|
-
// weeks — the desktop serialised `recall` and `skill_open` that the extension overlapped.
|
|
171
|
-
export const PARALLEL_LOCAL_RE = /^(find$|history_|web_search$|weather$|get_result$|skill_open$|skill_file$|recall$|memory_recall$|meeting_live_transcript$)/;
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* May this call share a batch with its neighbours? Read-only by its traits, not pinned
|
|
175
|
-
* serial by the toolset, and either remote (its own server) or on the local overlap list.
|
|
176
|
-
*/
|
|
177
|
-
export function parallelEligible(tools, call, traits) {
|
|
178
|
-
if (!traits?.readOnly) return false;
|
|
179
|
-
if (tools?.serialTools?.has(call.name)) return false;
|
|
180
|
-
const eff = effectiveToolName(call.name, call.input);
|
|
181
|
-
return !!tools?.remoteTools?.has(call.name) || PARALLEL_LOCAL_RE.test(eff) || PARALLEL_LOCAL_RE.test(call.name);
|
|
182
|
-
}
|
|
1
|
+
const w=new Set(["get","list","search","read","find","fetch","query","describe","lookup","look","show","view","count","check","status","recall","inspect","screenshot","preview","browse","grep","glob","head","tail","cat","ls","stat","resolve","detect","validate","explain","summarize","summarise","summary","history","recent","latest","whoami","info","render","compare","diff","ping","health","select","retrieve","load","peek","watch","tools","schema","suggest","smart"]),b=new Set(["create","update","set","post","put","send","write","add","edit","move","rename","upload","run","execute","exec","click","type","submit","save","insert","patch","apply","merge","push","commit","start","stop","enable","disable","assign","close","reopen","login","logout","install","replace","transfer","pay","order","book","schedule","publish","deploy","restart","import","sync","remember","label","tag","mark","fill","press","scroll","navigate","open","goto","drag","invoke","call","trigger","act"]),v=new Set(["delete","remove","drop","purge","destroy","reset","clear","kill","wipe","revoke","truncate","erase","overwrite","rm","rmdir","uninstall","terminate","ban","force","unlink","discard","forget"]);function k(e){const t=String(e||""),r=t.lastIndexOf("__");return r>=0?t.slice(r+2):t.replace(/^mcp[_-]/i,"")}const x=e=>k(e).toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);function _(e){return!!e&&typeof e=="object"&&["readOnlyHint","destructiveHint","idempotentHint","openWorldHint"].some(t=>typeof e[t]=="boolean")}function h(e){const t=typeof e=="string"?e:e?.name,r=typeof e=="object"&&e?e.annotations:null,c=x(t),l=c.some(o=>v.has(o)),d=!(l||c.some(o=>b.has(o)))&&c.some(o=>w.has(o));if(_(r)){const o=r.readOnlyHint===!0,f=o?!1:typeof r.destructiveHint=="boolean"?r.destructiveHint:l;return{readOnly:o,destructive:f,idempotent:o||r.idempotentHint===!0,openWorld:typeof r.openWorldHint=="boolean"?r.openWorldHint:!0,source:"annotations"}}return{readOnly:d,destructive:l,idempotent:d,openWorld:!0,source:"heuristic"}}const H=e=>!!e?.readOnly,S=e=>!!e?.readOnly,E=e=>e?.destructive===!0;function $(e=[]){const t=new Map;for(const r of e)r?.name&&t.set(r.name,h(r));return t}function R(e,{confirm:t=null,only:r=()=>!0,traitsOf:c=null,effectiveName:l=O}={}){if(!e||typeof e.execute!="function")return e;const u=e.execute.bind(e),d=new Map((e.specs||[]).filter(n=>n?.name).map(n=>[n.name,n])),o=new Set,f=c||((n,s)=>{const i=l(n,s);return e.traits?.get(i)||e.traits?.get(n)||h(d.get(i)||d.get(n)||{name:i})});return{...e,async execute(n,s,i){if(!r(n))return u(n,s,i);const m=f(n,s);if(!E(m))return u(n,s,i);const a=l(n,s);if(o.has(a))return u(n,s,i);if(!t)return JSON.stringify({error:`"${a}" is a destructive action and needs the user's confirmation, which this surface cannot ask for. Do not retry it; tell the user what you would do and let them run it from the side panel.`,blocked:!0,needsConfirmation:!0,tool:a});const p=await t({name:a,input:s,via:n===a?null:n,traits:m});return p==="always"&&o.add(a),p!=="allow"&&p!=="always"?JSON.stringify({error:`The user DECLINED "${a}". Do not retry it — stop and ask the user how to proceed.`,blocked:!0,declined:!0,tool:a}):u(n,s,i)}}}function y(e,t){const r=t&&typeof t=="object"?t.action:null;return typeof r=="string"&&r?r:e}const O=y,g=/^(find$|history_|web_search$|weather$|get_result$|skill_open$|skill_file$|recall$|memory_recall$|meeting_live_transcript$)/;function T(e,t,r){if(!r?.readOnly||e?.serialTools?.has(t.name))return!1;const c=y(t.name,t.input);return!!e?.remoteTools?.has(t.name)||g.test(c)||g.test(t.name)}export{g as PARALLEL_LOCAL_RE,k as bareToolName,H as canRunConcurrently,y as effectiveToolName,S as isCacheable,E as needsConfirmation,T as parallelEligible,h as toolTraits,$ as traitsIndex,R as withDestructiveGate};
|
package/toolset.js
CHANGED
|
@@ -1,89 +1,3 @@
|
|
|
1
|
-
|
|
2
|
-
// loop consumes: `{ specs, execute, system }`.
|
|
3
|
-
//
|
|
4
|
-
// A provider is `{ specs: ToolSpec[], execute(name, input, meta) => string | {text, note,
|
|
5
|
-
// image}, system?: string, remote?: boolean, serial?: boolean, traits?: Map, reach?: [] }`.
|
|
6
|
-
// ToolSpec is `{ name, description, parameters (JSON schema), annotations? }`.
|
|
7
|
-
//
|
|
8
|
-
// This lived in the extension for as long as the extension was the only client with a tool
|
|
9
|
-
// loop. The desktop grew one, and the second copy of "first provider to claim a name wins"
|
|
10
|
-
// would have been the second place that rule could quietly differ. Everything here is
|
|
11
|
-
// input → output; the one platform-flavoured thing — the shared MCP guidance a client
|
|
12
|
-
// prepends when any `mcp_*` tool is present — is INJECTED, so this file carries no prompt
|
|
13
|
-
// text of its own and stays off the extension's first-paint budget by exactly the bytes
|
|
14
|
-
// the old copy cost.
|
|
1
|
+
const l=/^mcp[_-]/i;function S(g,{mcpSystem:i=""}={}){const s=(g||[]).filter(t=>t&&t.specs?.length);if(!s.length)return;const o=[],a=new Map,h=new Set,c=new Map,m=new Set,d=[],f=new Map;for(const t of s){const n=t.remote===!0;if(t.traits instanceof Map)for(const[e,r]of t.traits)c.has(e)||c.set(e,r);if(Array.isArray(t.reach)&&t.specs.length===1)for(const e of t.reach)e?.name&&!f.has(e.name)&&(f.set(e.name,t.specs[0].name),d.push(e));for(const e of t.specs)a.has(e.name)||(o.push(e),a.set(e.name,t.execute),(n||l.test(String(e.name||"")))&&h.add(e.name),t.serial===!0&&m.add(e.name))}if(!o.length)return;const p=o.some(t=>l.test(String(t?.name||"")))?String((typeof i=="function"?i():i)||""):"",M=[p,...s.map(t=>t.system)].map(t=>String(t||"").trim()).filter(Boolean).join(`
|
|
15
2
|
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param providers the tool providers, in the order the model should read them
|
|
20
|
-
* @param mcpSystem the shared MCP rules — a string, or a function returning one, consulted
|
|
21
|
-
* only when an `mcp_*` tool is present so a turn without MCP pays nothing
|
|
22
|
-
* @returns the merged toolset, or `undefined` when no provider brought a tool
|
|
23
|
-
*/
|
|
24
|
-
export function buildToolset(providers, { mcpSystem = '' } = {}) {
|
|
25
|
-
const list = (providers || []).filter((p) => p && p.specs?.length);
|
|
26
|
-
if (!list.length) return undefined;
|
|
27
|
-
|
|
28
|
-
const specs = [];
|
|
29
|
-
const route = new Map(); // tool name -> the provider.execute that owns it
|
|
30
|
-
// Tools that call a REMOTE server — from a provider flagged remote, or (fallback) whose
|
|
31
|
-
// name matches the mcp_ convention. The PII harness uses this exact set to keep private
|
|
32
|
-
// data off remote tools under "redact remote".
|
|
33
|
-
const remoteTools = new Set();
|
|
34
|
-
// What each HIDDEN tool does — a dispatcher's own index of the tools behind it
|
|
35
|
-
// (tool-traits.js). Top-level specs carry `annotations` and are classified at run time.
|
|
36
|
-
const traits = new Map();
|
|
37
|
-
// Tools that must run one at a time even when read-only: page tools share ONE tab.
|
|
38
|
-
const serialTools = new Set();
|
|
39
|
-
// The tools a dispatcher hides, and which dispatcher: a recipe step names the real tool.
|
|
40
|
-
const reach = [];
|
|
41
|
-
const hiddenVia = new Map();
|
|
42
|
-
for (const p of list) {
|
|
43
|
-
const providerRemote = p.remote === true;
|
|
44
|
-
if (p.traits instanceof Map) for (const [k, v] of p.traits) if (!traits.has(k)) traits.set(k, v);
|
|
45
|
-
if (Array.isArray(p.reach) && p.specs.length === 1) {
|
|
46
|
-
for (const h of p.reach) if (h?.name && !hiddenVia.has(h.name)) { hiddenVia.set(h.name, p.specs[0].name); reach.push(h); }
|
|
47
|
-
}
|
|
48
|
-
for (const s of p.specs) {
|
|
49
|
-
if (route.has(s.name)) continue; // first provider to claim a name wins
|
|
50
|
-
specs.push(s);
|
|
51
|
-
route.set(s.name, p.execute);
|
|
52
|
-
if (providerRemote || REMOTE_NAME_RE.test(String(s.name || ''))) remoteTools.add(s.name);
|
|
53
|
-
if (p.serial === true) serialTools.add(s.name);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
if (!specs.length) return undefined;
|
|
57
|
-
|
|
58
|
-
// Generic MCP rules ONCE (not repeated per server), then each provider's own inventory.
|
|
59
|
-
const hasMcp = specs.some((s) => REMOTE_NAME_RE.test(String(s?.name || '')));
|
|
60
|
-
const shared = hasMcp ? String((typeof mcpSystem === 'function' ? mcpSystem() : mcpSystem) || '') : '';
|
|
61
|
-
const parts = [shared, ...list.map((p) => p.system)];
|
|
62
|
-
const system = parts.map((x) => String(x || '').trim()).filter(Boolean).join('\n\n') || undefined;
|
|
63
|
-
// WHICH blurb costs what — one total for the whole preamble is visible but unattributable,
|
|
64
|
-
// and a number nobody can attribute is a number nobody can reduce.
|
|
65
|
-
const systemParts = {};
|
|
66
|
-
if (shared.trim()) systemParts.mcp = Math.round(shared.length / 4);
|
|
67
|
-
for (const p of list) {
|
|
68
|
-
const t = Math.round(String(p.system || '').trim().length / 4);
|
|
69
|
-
// Named by the dispatcher tool it owns — 'page', 'find', 'mcp' — which is what the
|
|
70
|
-
// reader sees in the tools list and can act on.
|
|
71
|
-
if (t) systemParts[p.id || p.specs[0]?.name || 'group'] = t;
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
return {
|
|
75
|
-
specs,
|
|
76
|
-
system,
|
|
77
|
-
systemParts,
|
|
78
|
-
remoteTools,
|
|
79
|
-
traits,
|
|
80
|
-
serialTools,
|
|
81
|
-
reach,
|
|
82
|
-
hiddenVia,
|
|
83
|
-
async execute(name, input, meta = {}) {
|
|
84
|
-
const fn = route.get(name);
|
|
85
|
-
if (!fn) return JSON.stringify({ error: `Unknown tool: ${name}` });
|
|
86
|
-
return fn(name, input, meta);
|
|
87
|
-
},
|
|
88
|
-
};
|
|
89
|
-
}
|
|
3
|
+
`)||void 0,u={};p.trim()&&(u.mcp=Math.round(p.length/4));for(const t of s){const n=Math.round(String(t.system||"").trim().length/4);n&&(u[t.id||t.specs[0]?.name||"group"]=n)}return{specs:o,system:M,systemParts:u,remoteTools:h,traits:c,serialTools:m,reach:d,hiddenVia:f,async execute(t,n,e={}){const r=a.get(t);return r?r(t,n,e):JSON.stringify({error:`Unknown tool: ${t}`})}}}export{S as buildToolset};
|