@chatpanel/events 0.63.0 → 0.65.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +13 -1
- package/mcp-errors.js +19 -0
- package/meeting-shape.js +262 -0
- package/meeting-text.js +5 -46
- package/package.json +20 -6
- package/recipe.js +264 -0
- package/tool-discovery.js +87 -0
- package/tool-result.js +438 -0
- package/tool-round.js +0 -0
- package/tool-schema.js +155 -0
- package/tool-traits.js +101 -0
package/tool-traits.js
ADDED
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
// What a tool DOES to the world — read from its annotations, guessed from its name, and
|
|
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
|
+
}
|