boondmanager-mcp-server 2.9.0 → 2.11.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/README.md +73 -7
- package/dist/config/access-policy.d.ts +5 -0
- package/dist/config/access-policy.d.ts.map +1 -1
- package/dist/config/access-policy.js +36 -1
- package/dist/config/access-policy.js.map +1 -1
- package/dist/config/env-flags.d.ts +11 -0
- package/dist/config/env-flags.d.ts.map +1 -0
- package/dist/config/env-flags.js +16 -0
- package/dist/config/env-flags.js.map +1 -0
- package/dist/config/profiles.d.ts +71 -0
- package/dist/config/profiles.d.ts.map +1 -0
- package/dist/config/profiles.js +115 -0
- package/dist/config/profiles.js.map +1 -0
- package/dist/icons.d.ts +79 -0
- package/dist/icons.d.ts.map +1 -0
- package/dist/icons.js +169 -0
- package/dist/icons.js.map +1 -0
- package/dist/instructions.d.ts +21 -0
- package/dist/instructions.d.ts.map +1 -0
- package/dist/instructions.js +56 -0
- package/dist/instructions.js.map +1 -0
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +6 -0
- package/dist/resources/index.js.map +1 -1
- package/dist/schemas/filter-aliases.d.ts +69 -0
- package/dist/schemas/filter-aliases.d.ts.map +1 -0
- package/dist/schemas/filter-aliases.js +225 -0
- package/dist/schemas/filter-aliases.js.map +1 -0
- package/dist/schemas/index.d.ts +22 -22
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +54 -43
- package/dist/schemas/index.js.map +1 -1
- package/dist/server.d.ts +25 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +61 -16
- package/dist/server.js.map +1 -1
- package/dist/services/boond-client.d.ts.map +1 -1
- package/dist/services/boond-client.js +17 -2
- package/dist/services/boond-client.js.map +1 -1
- package/dist/tools/candidates.d.ts.map +1 -1
- package/dist/tools/candidates.js +1 -4
- package/dist/tools/candidates.js.map +1 -1
- package/dist/tools/companies.d.ts.map +1 -1
- package/dist/tools/companies.js +1 -4
- package/dist/tools/companies.js.map +1 -1
- package/dist/tools/contacts.d.ts.map +1 -1
- package/dist/tools/contacts.js +1 -4
- package/dist/tools/contacts.js.map +1 -1
- package/dist/tools/crud-factory.d.ts +0 -8
- package/dist/tools/crud-factory.d.ts.map +1 -1
- package/dist/tools/crud-factory.js +67 -15
- package/dist/tools/crud-factory.js.map +1 -1
- package/dist/tools/opportunities.d.ts.map +1 -1
- package/dist/tools/opportunities.js +1 -4
- package/dist/tools/opportunities.js.map +1 -1
- package/dist/tools/projects.d.ts.map +1 -1
- package/dist/tools/projects.js +1 -4
- package/dist/tools/projects.js.map +1 -1
- package/dist/tools/registration-decorators.d.ts +35 -0
- package/dist/tools/registration-decorators.d.ts.map +1 -0
- package/dist/tools/registration-decorators.js +30 -0
- package/dist/tools/registration-decorators.js.map +1 -0
- package/dist/tools/resources.d.ts.map +1 -1
- package/dist/tools/resources.js +1 -4
- package/dist/tools/resources.js.map +1 -1
- package/dist/tools/validation-wrapper.d.ts +43 -0
- package/dist/tools/validation-wrapper.d.ts.map +1 -0
- package/dist/tools/validation-wrapper.js +108 -0
- package/dist/tools/validation-wrapper.js.map +1 -0
- package/dist/transports/http.d.ts +57 -3
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +145 -9
- package/dist/transports/http.js.map +1 -1
- package/manifest.json +9 -2
- package/package.json +4 -2
package/dist/icons.js
ADDED
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { ListPromptsRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { PROMPTS } from "./prompts/index.js";
|
|
3
|
+
import { isFeatureDisabled } from "./config/env-flags.js";
|
|
4
|
+
/**
|
|
5
|
+
* Mid-grey: legible on both light and dark client themes. A data-URI SVG can't
|
|
6
|
+
* inherit `currentColor` from the host UI, so a single neutral tone beats
|
|
7
|
+
* shipping a light/dark pair (which would double the payload).
|
|
8
|
+
*/
|
|
9
|
+
const ICON_COLOR = "%237d8794";
|
|
10
|
+
/**
|
|
11
|
+
* `BOOND_MCP_ICONS=0|false|no|off` drops icons everywhere. Measured cost of the
|
|
12
|
+
* full set: ~40 KiB, i.e. ~14% of the 295 KiB `tools/list` payload (180 tools ×
|
|
13
|
+
* ~230 B). Worth it for a client that renders them, pure overhead for a gateway
|
|
14
|
+
* that doesn't — hence the switch.
|
|
15
|
+
*/
|
|
16
|
+
function iconsDisabled() {
|
|
17
|
+
return isFeatureDisabled(process.env.BOOND_MCP_ICONS);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Build the data URI for a single-path 16×16 glyph.
|
|
21
|
+
*
|
|
22
|
+
* Path data uses `,` as its number separator rather than a space: both are
|
|
23
|
+
* valid SVG, and a comma needs no percent-encoding inside a `data:` URI (a
|
|
24
|
+
* space does). The markup's own spaces are written as `%20` so the URI stays
|
|
25
|
+
* valid unquoted.
|
|
26
|
+
*/
|
|
27
|
+
function glyph(path) {
|
|
28
|
+
return {
|
|
29
|
+
src: `data:image/svg+xml,%3Csvg%20xmlns='http://www.w3.org/2000/svg'%20viewBox='0%200%2016%2016'` +
|
|
30
|
+
`%20fill='${ICON_COLOR}'%3E%3Cpath%20d='${path.replace(/ /g, ",")}'/%3E%3C/svg%3E`,
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
// ---- Glyph vocabulary (reused across domains) ----
|
|
34
|
+
const PERSON = glyph("M8 3a2.5 2.5 0 100 5 2.5 2.5 0 000-5zM3 14v-1c0-1.7 2.2-3 5-3s5 1.3 5 3v1z");
|
|
35
|
+
const PEOPLE = glyph("M5 4a2 2 0 100 4 2 2 0 000-4zm6 0a2 2 0 100 4 2 2 0 000-4zM1 14v-1c0-1.4 1.8-2.5 4-2.5s4 1.1 4 2.5v1zm8 0v-1c0-1 .7-1.9 1.9-2.4 1.8.2 3.1 1.2 3.1 2.4v1z");
|
|
36
|
+
const BUILDING = glyph("M3 2h10v12H3zm2 2h2v2H5zm4 0h2v2H9zM5 8h2v2H5zm4 0h2v2H9zm-2 4h2v2H7z");
|
|
37
|
+
const TARGET = glyph("M8 2a6 6 0 100 12A6 6 0 008 2zm0 4a2 2 0 100 4 2 2 0 000-4z");
|
|
38
|
+
const BRIEFCASE = glyph("M6 2h4v2h3v9H3V4h3zm1 1v1h2V3z");
|
|
39
|
+
const DOCUMENT = glyph("M4 1h5l3 3v11H4zm1 5h6v1H5zm0 3h6v1H5zm0 3h4v1H5z");
|
|
40
|
+
const CALENDAR = glyph("M3 3h10v11H3zm2-2h1v3H5zm5 0h1v3h-1zM4 6h8v1H4z");
|
|
41
|
+
const CLOCK = glyph("M8 2a6 6 0 100 12A6 6 0 008 2zm-.5 2h1v3.7l2.3 1.4-.5.9L7.5 8.4z");
|
|
42
|
+
const MONEY = glyph("M2 4h12v8H2zm6 1.5A2.5 2.5 0 118 10.5a2.5 2.5 0 010-5z");
|
|
43
|
+
const CART = glyph("M1 2h2l2 8h8l1-5H5v-1h10l-2 8H4L2 3H1zm4 10h1.5v1.5H5zm6 0h1.5v1.5H11z");
|
|
44
|
+
const BOX = glyph("M8 1l6 3-6 3-6-3zM2 6l6 3v6l-6-3zm12 0v6l-6 3V9z");
|
|
45
|
+
const CHART = glyph("M2 13h12v1H2zm1-5h2v4H3zm4-4h2v8H7zm4 2h2v6h-2z");
|
|
46
|
+
const LIST = glyph("M2 3h12v2H2zm0 4h12v2H2zm0 4h12v2H2z");
|
|
47
|
+
const CHECK = glyph("M6.5 13L2 8.5l1.4-1.4 3.1 3.1L12.6 4l1.4 1.4z");
|
|
48
|
+
const TAG = glyph("M2 2h6l6 6-6 6-6-6zm2 2v2h2V4z");
|
|
49
|
+
const GEAR = glyph("M7 1h2v2H7zm0 12h2v2H7zM1 7h2v2H1zm12 0h2v2h-2zM8 4.5a3.5 3.5 0 100 7 3.5 3.5 0 000-7zm0 2a1.5 1.5 0 110 3 1.5 1.5 0 010-3z");
|
|
50
|
+
const BELL = glyph("M8 1a4 4 0 00-4 4v4l-1 2h10l-1-2V5a4 4 0 00-4-4zm-1.5 12h3a1.5 1.5 0 01-3 0z");
|
|
51
|
+
const CHAT = glyph("M2 3h12v8H7l-3 3v-3H2z");
|
|
52
|
+
const PLUG = glyph("M4 1h2v4H4zm6 0h2v4h-2zM3 6h10v2a5 5 0 01-4 4.9V15H7v-2.1A5 5 0 013 8z");
|
|
53
|
+
const KEY = glyph("M10 2a4 4 0 00-3.9 5L2 11.1V14h3v-2h2v-1.2l.9-.9A4 4 0 1010 2zm1 2.2a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z");
|
|
54
|
+
const PIN = glyph("M8 1a4.5 4.5 0 00-4.5 4.5C3.5 9 8 15 8 15s4.5-6 4.5-9.5A4.5 4.5 0 008 1zm0 2.8a1.8 1.8 0 110 3.6 1.8 1.8 0 010-3.6z");
|
|
55
|
+
const PLANE = glyph("M8 1l1.2 5.5L15 8l-5.8 1.5L8 15l-1.2-5.5L1 8l5.8-1.5z");
|
|
56
|
+
/**
|
|
57
|
+
* Domain → icons. Every entry of `REGISTERED_DOMAINS` must be present
|
|
58
|
+
* (asserted in `icons.test.ts`), so adding a domain without an icon fails CI
|
|
59
|
+
* rather than shipping a half-iconified catalogue.
|
|
60
|
+
*/
|
|
61
|
+
export const DOMAIN_ICONS = {
|
|
62
|
+
candidates: [PERSON],
|
|
63
|
+
resources: [PEOPLE],
|
|
64
|
+
contacts: [PERSON],
|
|
65
|
+
companies: [BUILDING],
|
|
66
|
+
opportunities: [TARGET],
|
|
67
|
+
actions: [CHAT],
|
|
68
|
+
timesheets: [CLOCK],
|
|
69
|
+
projects: [BRIEFCASE],
|
|
70
|
+
invoices: [DOCUMENT],
|
|
71
|
+
orders: [DOCUMENT],
|
|
72
|
+
deliveries: [BRIEFCASE],
|
|
73
|
+
absences: [PLANE],
|
|
74
|
+
expenses: [MONEY],
|
|
75
|
+
products: [BOX],
|
|
76
|
+
positionings: [TARGET],
|
|
77
|
+
payments: [MONEY],
|
|
78
|
+
advantages: [MONEY],
|
|
79
|
+
application: [GEAR],
|
|
80
|
+
contracts: [DOCUMENT],
|
|
81
|
+
purchases: [CART],
|
|
82
|
+
"provider-invoices": [DOCUMENT],
|
|
83
|
+
accounts: [KEY],
|
|
84
|
+
agencies: [BUILDING],
|
|
85
|
+
"business-units": [BUILDING],
|
|
86
|
+
roles: [KEY],
|
|
87
|
+
logs: [LIST],
|
|
88
|
+
notifications: [BELL],
|
|
89
|
+
threads: [CHAT],
|
|
90
|
+
todolists: [CHECK],
|
|
91
|
+
flags: [TAG],
|
|
92
|
+
calendars: [CALENDAR],
|
|
93
|
+
webhooks: [PLUG],
|
|
94
|
+
validations: [CHECK],
|
|
95
|
+
poles: [PIN],
|
|
96
|
+
reporting: [CHART],
|
|
97
|
+
"planning-absences": [CALENDAR],
|
|
98
|
+
documents: [DOCUMENT],
|
|
99
|
+
workflows: [LIST],
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Icons for the dictionary / reference resources (`boond://…`). Resolved
|
|
103
|
+
* through a function, not a constant, because `registerAllResources` runs at
|
|
104
|
+
* startup and must honour `BOOND_MCP_ICONS` too.
|
|
105
|
+
*/
|
|
106
|
+
export function referenceIcons() {
|
|
107
|
+
return iconsDisabled() ? undefined : [LIST];
|
|
108
|
+
}
|
|
109
|
+
/** Icons for the `current-user` resource. */
|
|
110
|
+
export function identityIcons() {
|
|
111
|
+
return iconsDisabled() ? undefined : [PERSON];
|
|
112
|
+
}
|
|
113
|
+
export function iconsForDomain(domain) {
|
|
114
|
+
if (domain === undefined || iconsDisabled())
|
|
115
|
+
return undefined;
|
|
116
|
+
return DOMAIN_ICONS[domain];
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Prompt → icons, from the first domain the prompt orchestrates (its subject:
|
|
120
|
+
* `factures_a_relancer` → invoices, `synthese_equipe` → resources).
|
|
121
|
+
*/
|
|
122
|
+
export function iconsForPrompt(name) {
|
|
123
|
+
const prompt = PROMPTS.find((p) => p.name === name);
|
|
124
|
+
return iconsForDomain(prompt?.domains[0]);
|
|
125
|
+
}
|
|
126
|
+
/** Total byte cost of an icon payload, for the cap test and the docs. */
|
|
127
|
+
export function iconsByteSize(icons) {
|
|
128
|
+
return icons === undefined ? 0 : JSON.stringify(icons).length;
|
|
129
|
+
}
|
|
130
|
+
function withIcons(entry, icons) {
|
|
131
|
+
return icons === undefined || icons.length === 0 ? entry : { ...entry, icons };
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Attach domain icons to `tools/list` and `prompts/list` responses.
|
|
135
|
+
*
|
|
136
|
+
* MUST be called before the first `registerTool` / `registerPrompt` on this
|
|
137
|
+
* server: it works by intercepting the `setRequestHandler` calls the SDK makes
|
|
138
|
+
* when it lazily installs those handlers. `index` is read at request time, so
|
|
139
|
+
* it can still be empty at install time.
|
|
140
|
+
*/
|
|
141
|
+
export function installProtocolIcons(server, index) {
|
|
142
|
+
// `Server.setRequestHandler` is generic over the request schema; the shim is
|
|
143
|
+
// schema-agnostic, hence the single cast at the boundary.
|
|
144
|
+
const underlying = server.server;
|
|
145
|
+
const original = underlying.setRequestHandler.bind(underlying);
|
|
146
|
+
underlying.setRequestHandler = (schema, handler) => {
|
|
147
|
+
if (schema === ListToolsRequestSchema) {
|
|
148
|
+
return original(schema, async (...args) => {
|
|
149
|
+
const result = (await handler(...args));
|
|
150
|
+
if (!Array.isArray(result?.tools))
|
|
151
|
+
return result;
|
|
152
|
+
return {
|
|
153
|
+
...result,
|
|
154
|
+
tools: result.tools.map((t) => withIcons(t, iconsForDomain(index.toolDomains.get(t.name)))),
|
|
155
|
+
};
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (schema === ListPromptsRequestSchema) {
|
|
159
|
+
return original(schema, async (...args) => {
|
|
160
|
+
const result = (await handler(...args));
|
|
161
|
+
if (!Array.isArray(result?.prompts))
|
|
162
|
+
return result;
|
|
163
|
+
return { ...result, prompts: result.prompts.map((p) => withIcons(p, iconsForPrompt(p.name))) };
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
return original(schema, handler);
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=icons.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"icons.js","sourceRoot":"","sources":["../src/icons.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-level instructions advertised to the client in the `initialize`
|
|
3
|
+
* result (`ServerOptions.instructions`, MCP 2025-11-25).
|
|
4
|
+
*
|
|
5
|
+
* Scope: **cross-cutting rules only**. With ~180 tools, anything that applies
|
|
6
|
+
* to every search tool belongs here once instead of being duplicated in each
|
|
7
|
+
* description (which the model pays for in the tools[] list on every turn).
|
|
8
|
+
* Per-tool specifics stay in the tool descriptions and in `src/schemas/index.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Rule of thumb when editing: a statement belongs here only if it is true of
|
|
11
|
+
* *every* tool it appears to cover. A filter that exists on six endpoints and
|
|
12
|
+
* not on the other thirty-two is a per-tool fact — stating it here as universal
|
|
13
|
+
* makes the model emit calls that either get a Zod rejection or, worse, come
|
|
14
|
+
* back empty and read as a real answer.
|
|
15
|
+
*
|
|
16
|
+
* Budget: capped and tested in `src/tools/descriptions.test.ts`
|
|
17
|
+
* (`MAX_SERVER_INSTRUCTIONS_LENGTH`) — these instructions compete with the
|
|
18
|
+
* tool catalogue for the same context window.
|
|
19
|
+
*/
|
|
20
|
+
export declare const SERVER_INSTRUCTIONS = "Acc\u00E8s aux donn\u00E9es ERP/CRM BoondManager (API JSON:API) : candidats, ressources (consultants), contacts, soci\u00E9t\u00E9s, opportunit\u00E9s, projets, CRA, factures, achats\u2026\n\n## Nommage des outils\n\n`boond_{domaine}_{op\u00E9ration}` \u2014 op\u00E9rations : `search`, `get`, `create`, `update`, `delete`. Les onglets d'une fiche sont des outils distincts : `boond_{domaine}_{onglet}` (ex. `boond_resources_technical_data`). Workflow habituel : un `search` pour obtenir l'id, puis `get` ou l'onglet voulu pour le d\u00E9tail.\n\n## Les filtres sont propres \u00E0 chaque endpoint\n\nLe sch\u00E9ma de chaque outil est la r\u00E9f\u00E9rence : **ne pas transposer un filtre d'un endpoint \u00E0 un autre**. Les sch\u00E9mas sont `.strict()`, donc un nom inconnu est rejet\u00E9 par une erreur de validation \u2014 mais ce rejet a deux causes possibles : mauvais nom de filtre, **ou** filtre non support\u00E9 par cet endpoint. Dans le second cas, ne pas r\u00E9essayer avec des variantes : reprendre la recherche avec les filtres que le sch\u00E9ma expose (souvent `keywords` seul), ou filtrer c\u00F4t\u00E9 client apr\u00E8s lecture.\n\n## P\u00E9rim\u00E8tre (les 6 recherches principales + `boond_reporting_*`)\n\nSur `resources`, `candidates`, `contacts`, `companies`, `opportunities`, `projects` et le reporting \u2014 pas sur les domaines de r\u00E9f\u00E9rence/administration :\n\n- \u00AB mes donn\u00E9es / mon \u00E9quipe / mon agence \u00BB \u2192 `perimeterDynamic` : `[\"data\"]` (mes donn\u00E9es), `[\"managers\"]` (mon N-1), `[\"agencies\"]`, `[\"poles\"]`, `[\"businessUnits\"]`\n- \u00AB l'\u00E9quipe de X \u00BB \u2192 `perimeterManagers: [<id de X>]`\n- p\u00E9rim\u00E8tre organisationnel explicite \u2192 `perimeterAgencies` / `perimeterPoles` / `perimeterBusinessUnits`, \u00E0 combiner avec `narrowPerimeter: true` pour un ET logique\n\n`mainManagers`, `agencies`, `poles`, `businessUnits` n'existent sur aucun endpoint.\n\n## Cibler une entit\u00E9 li\u00E9e\n\nVocabulaire des pr\u00E9fixes de `keywords` : `CSOC<id>` (soci\u00E9t\u00E9), `CCON<id>` (contact), `CAND<id>` (candidat), `COMP<id>` (ressource), `AO<id>` (opportunit\u00E9), `PRJ<id>` (projet), `MIS<id>` (mission), `PROD<id>` (produit), `CTR<id>` (contrat) \u2014 ex. `keywords: \"CSOC42\"`.\n\n**Chaque endpoint n'en accepte qu'un sous-ensemble, \u00E9num\u00E9r\u00E9 dans la description de l'outil.** `keywords` \u00E9tant du texte libre, un pr\u00E9fixe non support\u00E9 n'est pas rejet\u00E9 : il part en recherche plein texte et renvoie 0 r\u00E9sultat. Une page vide apr\u00E8s un pr\u00E9fixe hors liste ne veut donc pas dire \u00AB aucune entit\u00E9 li\u00E9e \u00BB \u2014 v\u00E9rifier la description de l'outil avant de conclure.\n\n`keywordsType` (recherches `resources`, `candidates`, `contacts`, `companies` uniquement) restreint `keywords` \u00E0 un champ ; les valeurs admises diff\u00E8rent par endpoint (`lastName`/`fullName`/`titleSkills`\u2026 sur les personnes, `name`/`phones`/`emails` sur les soci\u00E9t\u00E9s) \u2014 lire l'\u00E9num\u00E9ration du sch\u00E9ma. Sans lui, la recherche porte sur le champ par d\u00E9faut de l'endpoint (CV ou texte int\u00E9gral).\n\n## \u00C9conomie de contexte\n\n- `pageSize` : d\u00E9faut 30, maximum 500. `page` est plafonn\u00E9 \u00E0 100 : au-del\u00E0, affiner les filtres plut\u00F4t que paginer.\n- `fields: [\"title\", \"updateDate\", \u2026]` remplace le r\u00E9sum\u00E9 d'une ligne par les seuls attributs demand\u00E9s \u2014 \u00E0 utiliser d\u00E8s qu'une page de r\u00E9sultats est large. Disponible sur les outils de recherche, sauf `boond_timesheets_search` et `boond_reporting_*`.\n\n## \u00C9tats et types\n\nCes filtres attendent des identifiants **entiers** et leur nom est propre \u00E0 l'endpoint : `resourceStates`, `candidateStates`, `opportunityStates`, `projectStates`, `states` + `typesOf` (contacts), `states` seul (soci\u00E9t\u00E9s \u2014 il n'y a pas de filtre de type sur `/companies`). Pour les traduire en libell\u00E9s, lire les ressources `boond://dictionary/*` (states, typeOf, countries, currencies, languages) plut\u00F4t que d'appeler `boond_application_dictionary` : m\u00EAme contenu, sans consommer un appel d'outil.";
|
|
21
|
+
//# sourceMappingURL=instructions.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instructions.d.ts","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,eAAO,MAAM,mBAAmB,+oIAmCue,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Server-level instructions advertised to the client in the `initialize`
|
|
3
|
+
* result (`ServerOptions.instructions`, MCP 2025-11-25).
|
|
4
|
+
*
|
|
5
|
+
* Scope: **cross-cutting rules only**. With ~180 tools, anything that applies
|
|
6
|
+
* to every search tool belongs here once instead of being duplicated in each
|
|
7
|
+
* description (which the model pays for in the tools[] list on every turn).
|
|
8
|
+
* Per-tool specifics stay in the tool descriptions and in `src/schemas/index.ts`.
|
|
9
|
+
*
|
|
10
|
+
* Rule of thumb when editing: a statement belongs here only if it is true of
|
|
11
|
+
* *every* tool it appears to cover. A filter that exists on six endpoints and
|
|
12
|
+
* not on the other thirty-two is a per-tool fact — stating it here as universal
|
|
13
|
+
* makes the model emit calls that either get a Zod rejection or, worse, come
|
|
14
|
+
* back empty and read as a real answer.
|
|
15
|
+
*
|
|
16
|
+
* Budget: capped and tested in `src/tools/descriptions.test.ts`
|
|
17
|
+
* (`MAX_SERVER_INSTRUCTIONS_LENGTH`) — these instructions compete with the
|
|
18
|
+
* tool catalogue for the same context window.
|
|
19
|
+
*/
|
|
20
|
+
export const SERVER_INSTRUCTIONS = `Accès aux données ERP/CRM BoondManager (API JSON:API) : candidats, ressources (consultants), contacts, sociétés, opportunités, projets, CRA, factures, achats…
|
|
21
|
+
|
|
22
|
+
## Nommage des outils
|
|
23
|
+
|
|
24
|
+
\`boond_{domaine}_{opération}\` — opérations : \`search\`, \`get\`, \`create\`, \`update\`, \`delete\`. Les onglets d'une fiche sont des outils distincts : \`boond_{domaine}_{onglet}\` (ex. \`boond_resources_technical_data\`). Workflow habituel : un \`search\` pour obtenir l'id, puis \`get\` ou l'onglet voulu pour le détail.
|
|
25
|
+
|
|
26
|
+
## Les filtres sont propres à chaque endpoint
|
|
27
|
+
|
|
28
|
+
Le schéma de chaque outil est la référence : **ne pas transposer un filtre d'un endpoint à un autre**. Les schémas sont \`.strict()\`, donc un nom inconnu est rejeté par une erreur de validation — mais ce rejet a deux causes possibles : mauvais nom de filtre, **ou** filtre non supporté par cet endpoint. Dans le second cas, ne pas réessayer avec des variantes : reprendre la recherche avec les filtres que le schéma expose (souvent \`keywords\` seul), ou filtrer côté client après lecture.
|
|
29
|
+
|
|
30
|
+
## Périmètre (les 6 recherches principales + \`boond_reporting_*\`)
|
|
31
|
+
|
|
32
|
+
Sur \`resources\`, \`candidates\`, \`contacts\`, \`companies\`, \`opportunities\`, \`projects\` et le reporting — pas sur les domaines de référence/administration :
|
|
33
|
+
|
|
34
|
+
- « mes données / mon équipe / mon agence » → \`perimeterDynamic\` : \`["data"]\` (mes données), \`["managers"]\` (mon N-1), \`["agencies"]\`, \`["poles"]\`, \`["businessUnits"]\`
|
|
35
|
+
- « l'équipe de X » → \`perimeterManagers: [<id de X>]\`
|
|
36
|
+
- périmètre organisationnel explicite → \`perimeterAgencies\` / \`perimeterPoles\` / \`perimeterBusinessUnits\`, à combiner avec \`narrowPerimeter: true\` pour un ET logique
|
|
37
|
+
|
|
38
|
+
\`mainManagers\`, \`agencies\`, \`poles\`, \`businessUnits\` n'existent sur aucun endpoint.
|
|
39
|
+
|
|
40
|
+
## Cibler une entité liée
|
|
41
|
+
|
|
42
|
+
Vocabulaire des préfixes de \`keywords\` : \`CSOC<id>\` (société), \`CCON<id>\` (contact), \`CAND<id>\` (candidat), \`COMP<id>\` (ressource), \`AO<id>\` (opportunité), \`PRJ<id>\` (projet), \`MIS<id>\` (mission), \`PROD<id>\` (produit), \`CTR<id>\` (contrat) — ex. \`keywords: "CSOC42"\`.
|
|
43
|
+
|
|
44
|
+
**Chaque endpoint n'en accepte qu'un sous-ensemble, énuméré dans la description de l'outil.** \`keywords\` étant du texte libre, un préfixe non supporté n'est pas rejeté : il part en recherche plein texte et renvoie 0 résultat. Une page vide après un préfixe hors liste ne veut donc pas dire « aucune entité liée » — vérifier la description de l'outil avant de conclure.
|
|
45
|
+
|
|
46
|
+
\`keywordsType\` (recherches \`resources\`, \`candidates\`, \`contacts\`, \`companies\` uniquement) restreint \`keywords\` à un champ ; les valeurs admises diffèrent par endpoint (\`lastName\`/\`fullName\`/\`titleSkills\`… sur les personnes, \`name\`/\`phones\`/\`emails\` sur les sociétés) — lire l'énumération du schéma. Sans lui, la recherche porte sur le champ par défaut de l'endpoint (CV ou texte intégral).
|
|
47
|
+
|
|
48
|
+
## Économie de contexte
|
|
49
|
+
|
|
50
|
+
- \`pageSize\` : défaut 30, maximum 500. \`page\` est plafonné à 100 : au-delà, affiner les filtres plutôt que paginer.
|
|
51
|
+
- \`fields: ["title", "updateDate", …]\` remplace le résumé d'une ligne par les seuls attributs demandés — à utiliser dès qu'une page de résultats est large. Disponible sur les outils de recherche, sauf \`boond_timesheets_search\` et \`boond_reporting_*\`.
|
|
52
|
+
|
|
53
|
+
## États et types
|
|
54
|
+
|
|
55
|
+
Ces filtres attendent des identifiants **entiers** et leur nom est propre à l'endpoint : \`resourceStates\`, \`candidateStates\`, \`opportunityStates\`, \`projectStates\`, \`states\` + \`typesOf\` (contacts), \`states\` seul (sociétés — il n'y a pas de filtre de type sur \`/companies\`). Pour les traduire en libellés, lire les ressources \`boond://dictionary/*\` (states, typeOf, countries, currencies, languages) plutôt que d'appeler \`boond_application_dictionary\` : même contenu, sans consommer un appel d'outil.`;
|
|
56
|
+
//# sourceMappingURL=instructions.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instructions.js","sourceRoot":"","sources":["../src/instructions.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ugBAmCoe,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AA4KzE,8EAA8E;AAC9E,eAAO,MAAM,oBAAoB;;;;GAQhC,CAAC;AAEF,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA4F5D"}
|
package/dist/resources/index.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { apiRequest } from "../services/boond-client.js";
|
|
2
2
|
import { getDictionary, resolveDictionaryPath } from "../services/dictionary.js";
|
|
3
3
|
import { getDictionaryOverrides } from "../config/dictionary-overrides.js";
|
|
4
|
+
import { identityIcons, referenceIcons } from "../icons.js";
|
|
4
5
|
/**
|
|
5
6
|
* Curated dictionaries surfaced as static MCP resources. Slugs follow the
|
|
6
7
|
* historical "kind/entity" naming for backward compatibility with client URIs;
|
|
@@ -152,6 +153,9 @@ export function registerAllResources(server) {
|
|
|
152
153
|
title: dict.title,
|
|
153
154
|
description: dict.description,
|
|
154
155
|
mimeType: "application/json",
|
|
156
|
+
// SEP-973. Unlike tools/prompts, the SDK's resource listing spreads the
|
|
157
|
+
// whole config, so `icons` reaches the client with no shim needed.
|
|
158
|
+
icons: referenceIcons(),
|
|
155
159
|
}, async () => {
|
|
156
160
|
const { payload } = await getDictionary();
|
|
157
161
|
const node = resolveDictionaryPath(payload, dict.path);
|
|
@@ -180,6 +184,7 @@ export function registerAllResources(server) {
|
|
|
180
184
|
description: "Mapping libellé→ID configuré via BOOND_DICTIONARY_OVERRIDES (types d'action et états). " +
|
|
181
185
|
'Renvoie { "configured": false } si aucun override n\'est configuré.',
|
|
182
186
|
mimeType: "application/json",
|
|
187
|
+
icons: referenceIcons(),
|
|
183
188
|
}, () => {
|
|
184
189
|
const overrides = getDictionaryOverrides();
|
|
185
190
|
return Promise.resolve({
|
|
@@ -200,6 +205,7 @@ export function registerAllResources(server) {
|
|
|
200
205
|
description: "Profil de l'utilisateur authentifié auprès de l'API BoondManager (id, agence, permissions). " +
|
|
201
206
|
"Utile pour résoudre 'mon ID' avant un appel filtré par perimeterManagers.",
|
|
202
207
|
mimeType: "application/json",
|
|
208
|
+
icons: identityIcons(),
|
|
203
209
|
}, async () => {
|
|
204
210
|
const response = await apiRequest("/application/current-user");
|
|
205
211
|
return {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AA+
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/resources/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AACzD,OAAO,EAAE,aAAa,EAAE,qBAAqB,EAAE,MAAM,2BAA2B,CAAC;AACjF,OAAO,EAAE,sBAAsB,EAAE,MAAM,mCAAmC,CAAC;AAC3E,OAAO,EAAE,aAAa,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AA+B5D;;;;;;;;GAQG;AACH,MAAM,YAAY,GAAsB;IACtC,yEAAyE;IACzE;QACE,IAAI,EAAE,kBAAkB;QACxB,IAAI,EAAE,wBAAwB;QAC9B,KAAK,EAAE,kBAAkB;QACzB,WAAW,EAAE,kDAAkD;KAChE;IACD;QACE,IAAI,EAAE,mBAAmB;QACzB,IAAI,EAAE,yBAAyB;QAC/B,KAAK,EAAE,iBAAiB;QACxB,WAAW,EAAE,iCAAiC;KAC/C;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,IAAI,EAAE,uBAAuB;QAC7B,KAAK,EAAE,gBAAgB;QACvB,WAAW,EAAE,gCAAgC;KAC9C;IACD;QACE,IAAI,EAAE,kBAAkB;QACxB,IAAI,EAAE,uBAAuB;QAC7B,KAAK,EAAE,gBAAgB;QACvB,WAAW,EAAE,gCAAgC;KAC9C;IACD;QACE,IAAI,EAAE,sBAAsB;QAC5B,IAAI,EAAE,2BAA2B;QACjC,KAAK,EAAE,oBAAoB;QAC3B,WAAW,EAAE,+CAA+C;KAC7D;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,IAAI,EAAE,uBAAuB;QAC7B,KAAK,EAAE,eAAe;QACtB,WAAW,EAAE,uCAAuC;KACrD;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,IAAI,EAAE,uBAAuB;QAC7B,KAAK,EAAE,gBAAgB;QACvB,WAAW,EAAE,uCAAuC;KACrD;IACD;QACE,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,qBAAqB;QAC3B,KAAK,EAAE,wBAAwB;QAC/B,WAAW,EAAE,wCAAwC;KACtD;IACD;QACE,IAAI,EAAE,qBAAqB;QAC3B,IAAI,EAAE,2BAA2B;QACjC,KAAK,EAAE,uBAAuB;QAC9B,WAAW,EAAE,uCAAuC;KACrD;IACD,0EAA0E;IAC1E;QACE,IAAI,EAAE,kBAAkB;QACxB,IAAI,EAAE,yBAAyB;QAC/B,KAAK,EAAE,kBAAkB;QACzB,WAAW,EAAE,4DAA4D;KAC1E;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,IAAI,EAAE,wBAAwB;QAC9B,KAAK,EAAE,gBAAgB;QACvB,WAAW,EAAE,mBAAmB;KACjC;IACD;QACE,IAAI,EAAE,iBAAiB;QACvB,IAAI,EAAE,wBAAwB;QAC9B,KAAK,EAAE,eAAe;QACtB,WAAW,EAAE,+CAA+C;KAC7D;IACD,uBAAuB;IACvB;QACE,IAAI,EAAE,OAAO;QACb,IAAI,EAAE,cAAc;QACpB,KAAK,EAAE,kBAAkB;QACzB,WAAW,EAAE,oGAAoG;KAClH;IACD;QACE,IAAI,EAAE,gBAAgB;QACtB,IAAI,EAAE,uBAAuB;QAC7B,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EAAE,4DAA4D;KAC1E;IACD;QACE,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,oBAAoB;QAC1B,KAAK,EAAE,sBAAsB;QAC7B,WAAW,EAAE,uDAAuD;KACrE;IACD;QACE,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,sBAAsB;QAC5B,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EAAE,4CAA4C;KAC1D;IACD;QACE,IAAI,EAAE,eAAe;QACrB,IAAI,EAAE,sBAAsB;QAC5B,KAAK,EAAE,WAAW;QAClB,WAAW,EAAE,iCAAiC;KAC/C;IACD,iBAAiB;IACjB,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,WAAW,EAAE,wCAAwC,EAAE;IAC5G,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,kBAAkB,EAAE,KAAK,EAAE,SAAS,EAAE,WAAW,EAAE,+BAA+B,EAAE;IAChH;QACE,IAAI,EAAE,WAAW;QACjB,IAAI,EAAE,WAAW;QACjB,KAAK,EAAE,SAAS;QAChB,WAAW,EAAE,gDAAgD;KAC9D;CACF,CAAC;AAEF,2DAA2D;AAC3D,MAAM,qBAAqB,GAAG,qBAAqB,CAAC;AACpD,2CAA2C;AAC3C,MAAM,gBAAgB,GAAG,kCAAkC,CAAC;AAC5D,+EAA+E;AAC/E,MAAM,aAAa,GAAG,8BAA8B,CAAC;AAErD,SAAS,gBAAgB,CAAC,IAAY;IACpC,OAAO,GAAG,qBAAqB,GAAG,IAAI,EAAE,CAAC;AAC3C,CAAC;AAED,8EAA8E;AAC9E,MAAM,CAAC,MAAM,oBAAoB,GAAG;IAClC,GAAG,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QAC1B,IAAI,EAAE,cAAc,CAAC,CAAC,IAAI,EAAE;QAC5B,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,IAAI,CAAC;QAC7B,KAAK,EAAE,CAAC,CAAC,KAAK;KACf,CAAC,CAAC;IACH,EAAE,IAAI,EAAE,sBAAsB,EAAE,GAAG,EAAE,aAAa,EAAE,KAAK,EAAE,oCAAoC,EAAE;IACjG,EAAE,IAAI,EAAE,0BAA0B,EAAE,GAAG,EAAE,gBAAgB,EAAE,KAAK,EAAE,qBAAqB,EAAE;CAC1F,CAAC;AAEF,MAAM,UAAU,oBAAoB,CAAC,MAAiB;IACpD,KAAK,MAAM,IAAI,IAAI,YAAY,EAAE,CAAC;QAChC,MAAM,GAAG,GAAG,gBAAgB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;QACxC,MAAM,CAAC,gBAAgB,CACrB,cAAc,IAAI,CAAC,IAAI,EAAE,EACzB,GAAG,EACH;YACE,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,WAAW,EAAE,IAAI,CAAC,WAAW;YAC7B,QAAQ,EAAE,kBAAkB;YAC5B,wEAAwE;YACxE,mEAAmE;YACnE,KAAK,EAAE,cAAc,EAAE;SACxB,EACD,KAAK,IAAI,EAAE;YACT,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,aAAa,EAAE,CAAC;YAC1C,MAAM,IAAI,GAAG,qBAAqB,CAAC,OAAO,EAAE,IAAI,CAAC,IAAI,CAAC,CAAC;YACvD,MAAM,IAAI,GACR,IAAI,KAAK,SAAS;gBAChB,CAAC,CAAC;oBACE,KAAK,EAAE,SAAS,IAAI,CAAC,IAAI,mGAAmG;iBAC7H;gBACH,CAAC,CAAC,IAAI,CAAC;YACX,OAAO;gBACL,QAAQ,EAAE;oBACR;wBACE,GAAG;wBACH,QAAQ,EAAE,kBAAkB;wBAC5B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,IAAI,EAAE,IAAI,EAAE,CAAC,CAAC;qBACpC;iBACF;aACF,CAAC;QACJ,CAAC,CACF,CAAC;IACJ,CAAC;IAED,yEAAyE;IACzE,8EAA8E;IAC9E,wEAAwE;IACxE,iEAAiE;IACjE,MAAM,CAAC,gBAAgB,CACrB,sBAAsB,EACtB,aAAa,EACb;QACE,KAAK,EAAE,oCAAoC;QAC3C,WAAW,EACT,yFAAyF;YACzF,qEAAqE;QACvE,QAAQ,EAAE,kBAAkB;QAC5B,KAAK,EAAE,cAAc,EAAE;KACxB,EACD,GAAG,EAAE;QACH,MAAM,SAAS,GAAG,sBAAsB,EAAE,CAAC;QAC3C,OAAO,OAAO,CAAC,OAAO,CAAC;YACrB,QAAQ,EAAE;gBACR;oBACE,GAAG,EAAE,aAAa;oBAClB,QAAQ,EAAE,kBAAkB;oBAC5B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,SAAS,IAAI,EAAE,UAAU,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;iBAClE;aACF;SACF,CAAC,CAAC;IACL,CAAC,CACF,CAAC;IAEF,yEAAyE;IACzE,yEAAyE;IACzE,0CAA0C;IAC1C,MAAM,CAAC,gBAAgB,CACrB,0BAA0B,EAC1B,gBAAgB,EAChB;QACE,KAAK,EAAE,qBAAqB;QAC5B,WAAW,EACT,8FAA8F;YAC9F,2EAA2E;QAC7E,QAAQ,EAAE,kBAAkB;QAC5B,KAAK,EAAE,aAAa,EAAE;KACvB,EACD,KAAK,IAAI,EAAE;QACT,MAAM,QAAQ,GAAG,MAAM,UAAU,CAAC,2BAA2B,CAAC,CAAC;QAC/D,OAAO;YACL,QAAQ,EAAE;gBACR;oBACE,GAAG,EAAE,gBAAgB;oBACrB,QAAQ,EAAE,kBAAkB;oBAC5B,IAAI,EAAE,IAAI,CAAC,SAAS,CAAC,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;iBACxC;aACF;SACF,CAAC;IACJ,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { DomainName } from "../constants.js";
|
|
2
|
+
/**
|
|
3
|
+
* Wrong-filter-name → correction table, used to turn a `.strict()` schema
|
|
4
|
+
* rejection into something the model can act on in one turn (SEP-1303: input
|
|
5
|
+
* validation failures must surface as *tool* errors so the model can
|
|
6
|
+
* self-correct, not as opaque protocol errors).
|
|
7
|
+
*
|
|
8
|
+
* Why this exists: the six main search endpoints take filter names that must
|
|
9
|
+
* match the BoondManager API query parameters verbatim (see CLAUDE.md
|
|
10
|
+
* §*Search Filter Naming*). The schemas are `.strict()`, so a wrong name is
|
|
11
|
+
* rejected rather than silently ignored — but "Unrecognized key:
|
|
12
|
+
* \"mainManagers\"" tells the model *that* it was wrong, never *what to use
|
|
13
|
+
* instead*. Every entry below is a confusion we have actually seen (or that
|
|
14
|
+
* the API's own naming invites), mapped to the correct name plus the one-line
|
|
15
|
+
* reason the correct name behaves differently.
|
|
16
|
+
*
|
|
17
|
+
* The messages are consumed by `unknownFilterMessage()`, which the search
|
|
18
|
+
* schemas install as their unrecognized-key error (see
|
|
19
|
+
* `src/tools/validation-wrapper.ts`). Keep them short: they are read by a
|
|
20
|
+
* model mid-call, not by a human reading docs.
|
|
21
|
+
*/
|
|
22
|
+
export interface FilterAlias {
|
|
23
|
+
/** Input name to use instead. Absent when the filter simply does not exist on that endpoint. */
|
|
24
|
+
correct?: string;
|
|
25
|
+
/** One-line reason / usage note. Should say what the correct filter *does*, not just its name. */
|
|
26
|
+
hint: string;
|
|
27
|
+
/** Dictionary resource URI to read when the value is a state/type id. */
|
|
28
|
+
dictionary?: string;
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Endpoints whose filter vocabulary differs (states/types are named after the
|
|
32
|
+
* entity). Everything else only gets the global table.
|
|
33
|
+
*/
|
|
34
|
+
export type SearchEndpoint = Extract<DomainName, "resources" | "candidates" | "contacts" | "companies" | "opportunities" | "projects">;
|
|
35
|
+
/** Confusions that are wrong on every endpoint. */
|
|
36
|
+
export declare const GLOBAL_FILTER_ALIASES: Readonly<Record<string, FilterAlias>>;
|
|
37
|
+
/**
|
|
38
|
+
* Endpoint-specific confusions. `states` and `typeOf` are the two names the
|
|
39
|
+
* model reaches for by default; on four of the six endpoints they are prefixed
|
|
40
|
+
* with the entity, on `/contacts` it is `typesOf` (with the s), and
|
|
41
|
+
* `/companies` has no type filter at all.
|
|
42
|
+
*/
|
|
43
|
+
export declare const ENDPOINT_FILTER_ALIASES: Readonly<Record<SearchEndpoint, Readonly<Record<string, FilterAlias>>>>;
|
|
44
|
+
/** Resolve a wrong filter name to its correction, endpoint-specific table first. */
|
|
45
|
+
export declare function resolveFilterAlias(key: string, endpoint?: SearchEndpoint): FilterAlias | undefined;
|
|
46
|
+
/**
|
|
47
|
+
* Closest accepted key within 2 edits (case/separator-insensitive), for typos
|
|
48
|
+
* the alias table doesn't know about (`pagesize`, `keywordType`, …).
|
|
49
|
+
*/
|
|
50
|
+
export declare function closestKey(key: string, validKeys: readonly string[]): string | undefined;
|
|
51
|
+
/**
|
|
52
|
+
* Build the text returned to the model when a search call carries unknown
|
|
53
|
+
* filter names. Shape, per unknown key:
|
|
54
|
+
*
|
|
55
|
+
* Filtre inconnu « mainManagers » → utiliser `perimeterManagers` (…).
|
|
56
|
+
*
|
|
57
|
+
* plus, when the correction is a state/type filter, the dictionary resource to
|
|
58
|
+
* read for the ids. Ends with the accepted filter list so the model can retry
|
|
59
|
+
* from the error alone rather than re-reading the whole tool schema.
|
|
60
|
+
*
|
|
61
|
+
* A correction is only printed when the replacement is **actually accepted by
|
|
62
|
+
* this endpoint**. The wrapper runs on every search tool, not just the six
|
|
63
|
+
* perimeter-aware ones, and the global table is written for those: telling
|
|
64
|
+
* `boond_invoices_search` to use `perimeterAgencies` (which it does not accept)
|
|
65
|
+
* sends the model into a second rejection, after which it typically drops the
|
|
66
|
+
* filter and reports a company-wide list as if it were scoped.
|
|
67
|
+
*/
|
|
68
|
+
export declare function unknownFilterMessage(keys: readonly string[], validKeys: readonly string[], endpoint?: SearchEndpoint): string;
|
|
69
|
+
//# sourceMappingURL=filter-aliases.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"filter-aliases.d.ts","sourceRoot":"","sources":["../../src/schemas/filter-aliases.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElD;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,MAAM,WAAW,WAAW;IAC1B,gGAAgG;IAChG,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,kGAAkG;IAClG,IAAI,EAAE,MAAM,CAAC;IACb,yEAAyE;IACzE,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,OAAO,CAClC,UAAU,EACV,WAAW,GAAG,YAAY,GAAG,UAAU,GAAG,WAAW,GAAG,eAAe,GAAG,UAAU,CACrF,CAAC;AAIF,mDAAmD;AACnD,eAAO,MAAM,qBAAqB,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAuCvE,CAAC;AAKF;;;;;GAKG;AACH,eAAO,MAAM,uBAAuB,EAAE,QAAQ,CAAC,MAAM,CAAC,cAAc,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC,CAAC,CA2D3G,CAAC;AAqBF,oFAAoF;AACpF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,cAAc,GAAG,WAAW,GAAG,SAAS,CAOlG;AAgBD;;;GAGG;AACH,wBAAgB,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,SAAS,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,GAAG,SAAS,CAUxF;AAaD;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,oBAAoB,CAClC,IAAI,EAAE,SAAS,MAAM,EAAE,EACvB,SAAS,EAAE,SAAS,MAAM,EAAE,EAC5B,QAAQ,CAAC,EAAE,cAAc,GACxB,MAAM,CAkCR"}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
const PERIMETER_DYNAMIC_NOTE = 'pour « mes données / mon équipe », `perimeterDynamic: ["data"|"managers"]`';
|
|
2
|
+
/** Confusions that are wrong on every endpoint. */
|
|
3
|
+
export const GLOBAL_FILTER_ALIASES = {
|
|
4
|
+
mainmanagers: {
|
|
5
|
+
correct: "perimeterManagers",
|
|
6
|
+
hint: `IDs des managers dont on veut l'équipe (${PERIMETER_DYNAMIC_NOTE})`,
|
|
7
|
+
},
|
|
8
|
+
managers: {
|
|
9
|
+
correct: "perimeterManagers",
|
|
10
|
+
hint: `IDs des managers dont on veut l'équipe (${PERIMETER_DYNAMIC_NOTE})`,
|
|
11
|
+
},
|
|
12
|
+
agencies: { correct: "perimeterAgencies", hint: "IDs d'agences ; combiner avec `narrowPerimeter: true` pour un ET" },
|
|
13
|
+
poles: { correct: "perimeterPoles", hint: "IDs de pôles" },
|
|
14
|
+
businessunits: { correct: "perimeterBusinessUnits", hint: "IDs de business units" },
|
|
15
|
+
// Pagination: `maxResults` is the *API* name; the tools expose `pageSize`.
|
|
16
|
+
maxresults: { correct: "pageSize", hint: "le nom API `maxResults` est dérivé côté serveur" },
|
|
17
|
+
limit: { correct: "pageSize", hint: "nombre de résultats par page" },
|
|
18
|
+
perpage: { correct: "pageSize", hint: "nombre de résultats par page" },
|
|
19
|
+
offset: { correct: "page", hint: "pagination par numéro de page, pas par offset" },
|
|
20
|
+
start: { correct: "page", hint: "pagination par numéro de page, pas par offset" },
|
|
21
|
+
sortby: { correct: "sort", hint: "nom du champ de tri" },
|
|
22
|
+
orderby: { correct: "order", hint: '"asc" ou "desc"' },
|
|
23
|
+
q: { correct: "keywords", hint: "recherche plein texte" },
|
|
24
|
+
query: { correct: "keywords", hint: "recherche plein texte" },
|
|
25
|
+
search: { correct: "keywords", hint: "recherche plein texte" },
|
|
26
|
+
name: { correct: "keywords", hint: 'cibler un champ précis via `keywordsType` (ex: "lastName", "fullName")' },
|
|
27
|
+
fullname: { correct: "keywords", hint: '`keywordsType: "fullName"` avec `keywords: "NOM#PRENOM"`' },
|
|
28
|
+
lastname: { correct: "keywords", hint: '`keywordsType: "lastName"`' },
|
|
29
|
+
firstname: { correct: "keywords", hint: '`keywordsType: "firstName"`' },
|
|
30
|
+
email: { correct: "keywords", hint: '`keywordsType: "emails"`' },
|
|
31
|
+
emails: { correct: "keywords", hint: '`keywordsType: "emails"`' },
|
|
32
|
+
// Linked-entity lookups go through the `keywords` prefix syntax.
|
|
33
|
+
companyid: {
|
|
34
|
+
correct: "keywords",
|
|
35
|
+
hint: 'entité liée : `keywords: "CSOC<id>"` (projets : filtre `companies: [<id>]`)',
|
|
36
|
+
},
|
|
37
|
+
contactid: { correct: "keywords", hint: 'entité liée : `keywords: "CCON<id>"`' },
|
|
38
|
+
candidateid: { correct: "keywords", hint: 'entité liée : `keywords: "CAND<id>"`' },
|
|
39
|
+
resourceid: { correct: "keywords", hint: 'entité liée : `keywords: "COMP<id>"`' },
|
|
40
|
+
opportunityid: { correct: "keywords", hint: 'entité liée : `keywords: "AO<id>"`' },
|
|
41
|
+
projectid: { correct: "keywords", hint: 'entité liée : `keywords: "PRJ<id>"`' },
|
|
42
|
+
};
|
|
43
|
+
const STATES_URI = (entity) => `boond://dictionary/states/${entity}`;
|
|
44
|
+
const TYPEOF_URI = (entity) => `boond://dictionary/typeOf/${entity}`;
|
|
45
|
+
/**
|
|
46
|
+
* Endpoint-specific confusions. `states` and `typeOf` are the two names the
|
|
47
|
+
* model reaches for by default; on four of the six endpoints they are prefixed
|
|
48
|
+
* with the entity, on `/contacts` it is `typesOf` (with the s), and
|
|
49
|
+
* `/companies` has no type filter at all.
|
|
50
|
+
*/
|
|
51
|
+
export const ENDPOINT_FILTER_ALIASES = {
|
|
52
|
+
resources: {
|
|
53
|
+
states: { correct: "resourceStates", hint: "IDs d'états (entiers)", dictionary: STATES_URI("resources") },
|
|
54
|
+
excludestates: {
|
|
55
|
+
correct: "excludeResourceStates",
|
|
56
|
+
hint: "IDs d'états à exclure",
|
|
57
|
+
dictionary: STATES_URI("resources"),
|
|
58
|
+
},
|
|
59
|
+
typeof: { correct: "resourceTypes", hint: "IDs de types (entiers)", dictionary: TYPEOF_URI("resources") },
|
|
60
|
+
types: { correct: "resourceTypes", hint: "IDs de types (entiers)", dictionary: TYPEOF_URI("resources") },
|
|
61
|
+
typesof: { correct: "resourceTypes", hint: "IDs de types (entiers)", dictionary: TYPEOF_URI("resources") },
|
|
62
|
+
},
|
|
63
|
+
candidates: {
|
|
64
|
+
states: { correct: "candidateStates", hint: "IDs d'états (entiers)", dictionary: STATES_URI("candidates") },
|
|
65
|
+
typeof: { correct: "candidateTypes", hint: "IDs de types (entiers)", dictionary: TYPEOF_URI("resources") },
|
|
66
|
+
types: { correct: "candidateTypes", hint: "IDs de types (entiers)", dictionary: TYPEOF_URI("resources") },
|
|
67
|
+
typesof: { correct: "candidateTypes", hint: "IDs de types (entiers)", dictionary: TYPEOF_URI("resources") },
|
|
68
|
+
},
|
|
69
|
+
contacts: {
|
|
70
|
+
typeof: {
|
|
71
|
+
correct: "typesOf",
|
|
72
|
+
hint: "avec un `s` : `typesOf` (IDs entiers)",
|
|
73
|
+
dictionary: TYPEOF_URI("contacts"),
|
|
74
|
+
},
|
|
75
|
+
types: { correct: "typesOf", hint: "IDs de types de contact (entiers)", dictionary: TYPEOF_URI("contacts") },
|
|
76
|
+
contactstates: {
|
|
77
|
+
correct: "states",
|
|
78
|
+
hint: "sur /contacts le filtre s'appelle `states`",
|
|
79
|
+
dictionary: STATES_URI("contacts"),
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
companies: {
|
|
83
|
+
typeof: {
|
|
84
|
+
hint: "/companies n'expose aucun filtre de type — filtrer sur `states`, `activityAreas` ou après lecture",
|
|
85
|
+
},
|
|
86
|
+
types: {
|
|
87
|
+
hint: "/companies n'expose aucun filtre de type — filtrer sur `states`, `activityAreas` ou après lecture",
|
|
88
|
+
},
|
|
89
|
+
// `typesOf` is the correct name on /contacts, so the model transfers it here.
|
|
90
|
+
typesof: {
|
|
91
|
+
hint: "/companies n'expose aucun filtre de type (`typesOf` n'existe que sur /contacts) — filtrer sur `states` ou `activityAreas`",
|
|
92
|
+
},
|
|
93
|
+
companystates: {
|
|
94
|
+
correct: "states",
|
|
95
|
+
hint: "sur /companies le filtre s'appelle `states`",
|
|
96
|
+
dictionary: STATES_URI("companies"),
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
opportunities: {
|
|
100
|
+
states: { correct: "opportunityStates", hint: "IDs d'états (entiers)", dictionary: STATES_URI("opportunities") },
|
|
101
|
+
typeof: { correct: "opportunityTypes", hint: "IDs de types", dictionary: TYPEOF_URI("projects") },
|
|
102
|
+
types: { correct: "opportunityTypes", hint: "IDs de types", dictionary: TYPEOF_URI("projects") },
|
|
103
|
+
},
|
|
104
|
+
projects: {
|
|
105
|
+
states: { correct: "projectStates", hint: "IDs d'états (entiers)", dictionary: STATES_URI("projects") },
|
|
106
|
+
typeof: { correct: "projectTypes", hint: "IDs de types (entiers)", dictionary: TYPEOF_URI("projects") },
|
|
107
|
+
types: { correct: "projectTypes", hint: "IDs de types (entiers)", dictionary: TYPEOF_URI("projects") },
|
|
108
|
+
company: { correct: "companies", hint: "tableau d'IDs de sociétés, ex: `companies: [42]`" },
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Lookup key: lowercase, separators stripped. So `max_results`, `maxResults`
|
|
113
|
+
* and `MAXRESULTS` all resolve to the same entry — the model's mistakes are
|
|
114
|
+
* about the *word*, not its casing.
|
|
115
|
+
*/
|
|
116
|
+
function normalizeKey(key) {
|
|
117
|
+
return key.toLowerCase().replace(/[-_\s]/g, "");
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Own-property lookup. The tables are object literals, so a plain `table[norm]`
|
|
121
|
+
* happily returns `Object.prototype.constructor` / `toString` / `hasOwnProperty`
|
|
122
|
+
* for a filter named after them — a truthy value with no `.correct`/`.hint`,
|
|
123
|
+
* which used to print "Filtre inconnu « constructor » : undefined.".
|
|
124
|
+
*/
|
|
125
|
+
function lookupAlias(table, norm) {
|
|
126
|
+
return Object.hasOwn(table, norm) ? table[norm] : undefined;
|
|
127
|
+
}
|
|
128
|
+
/** Resolve a wrong filter name to its correction, endpoint-specific table first. */
|
|
129
|
+
export function resolveFilterAlias(key, endpoint) {
|
|
130
|
+
const norm = normalizeKey(key);
|
|
131
|
+
if (endpoint !== undefined && Object.hasOwn(ENDPOINT_FILTER_ALIASES, endpoint)) {
|
|
132
|
+
const specific = lookupAlias(ENDPOINT_FILTER_ALIASES[endpoint], norm);
|
|
133
|
+
if (specific)
|
|
134
|
+
return specific;
|
|
135
|
+
}
|
|
136
|
+
return lookupAlias(GLOBAL_FILTER_ALIASES, norm);
|
|
137
|
+
}
|
|
138
|
+
/** Levenshtein distance, capped: we only care about "is it within 2 edits". */
|
|
139
|
+
function editDistance(a, b, max) {
|
|
140
|
+
if (Math.abs(a.length - b.length) > max)
|
|
141
|
+
return max + 1;
|
|
142
|
+
let prev = Array.from({ length: b.length + 1 }, (_, i) => i);
|
|
143
|
+
for (let i = 1; i <= a.length; i++) {
|
|
144
|
+
const row = [i];
|
|
145
|
+
for (let j = 1; j <= b.length; j++) {
|
|
146
|
+
row[j] = Math.min(prev[j] + 1, row[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
147
|
+
}
|
|
148
|
+
prev = row;
|
|
149
|
+
}
|
|
150
|
+
return prev[b.length];
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Closest accepted key within 2 edits (case/separator-insensitive), for typos
|
|
154
|
+
* the alias table doesn't know about (`pagesize`, `keywordType`, …).
|
|
155
|
+
*/
|
|
156
|
+
export function closestKey(key, validKeys) {
|
|
157
|
+
const norm = normalizeKey(key);
|
|
158
|
+
let best;
|
|
159
|
+
for (const candidate of validKeys) {
|
|
160
|
+
const distance = editDistance(norm, normalizeKey(candidate), 2);
|
|
161
|
+
if (distance <= 2 && (best === undefined || distance < best.distance)) {
|
|
162
|
+
best = { key: candidate, distance };
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
return best?.key;
|
|
166
|
+
}
|
|
167
|
+
/** Cap the accepted-filters list so one bad key can't produce a wall of text. */
|
|
168
|
+
const MAX_ACCEPTED_KEYS_LISTED = 25;
|
|
169
|
+
/**
|
|
170
|
+
* Cap the number of per-key correction lines, for the same reason: the message
|
|
171
|
+
* is read by a model mid-call. A stray 40-key filter object would otherwise emit
|
|
172
|
+
* 40 lines (and 40 full Levenshtein scans) — several thousand characters on a
|
|
173
|
+
* path whose whole point is a short, actionable correction.
|
|
174
|
+
*/
|
|
175
|
+
const MAX_UNKNOWN_KEYS_EXPLAINED = 6;
|
|
176
|
+
/**
|
|
177
|
+
* Build the text returned to the model when a search call carries unknown
|
|
178
|
+
* filter names. Shape, per unknown key:
|
|
179
|
+
*
|
|
180
|
+
* Filtre inconnu « mainManagers » → utiliser `perimeterManagers` (…).
|
|
181
|
+
*
|
|
182
|
+
* plus, when the correction is a state/type filter, the dictionary resource to
|
|
183
|
+
* read for the ids. Ends with the accepted filter list so the model can retry
|
|
184
|
+
* from the error alone rather than re-reading the whole tool schema.
|
|
185
|
+
*
|
|
186
|
+
* A correction is only printed when the replacement is **actually accepted by
|
|
187
|
+
* this endpoint**. The wrapper runs on every search tool, not just the six
|
|
188
|
+
* perimeter-aware ones, and the global table is written for those: telling
|
|
189
|
+
* `boond_invoices_search` to use `perimeterAgencies` (which it does not accept)
|
|
190
|
+
* sends the model into a second rejection, after which it typically drops the
|
|
191
|
+
* filter and reports a company-wide list as if it were scoped.
|
|
192
|
+
*/
|
|
193
|
+
export function unknownFilterMessage(keys, validKeys, endpoint) {
|
|
194
|
+
const lines = [];
|
|
195
|
+
const explained = keys.slice(0, MAX_UNKNOWN_KEYS_EXPLAINED);
|
|
196
|
+
for (const key of explained) {
|
|
197
|
+
const alias = resolveFilterAlias(key, endpoint);
|
|
198
|
+
if (alias?.correct !== undefined && validKeys.includes(alias.correct)) {
|
|
199
|
+
const dict = alias.dictionary ? ` — IDs via ${alias.dictionary}` : "";
|
|
200
|
+
lines.push(`Filtre inconnu « ${key} » → utiliser \`${alias.correct}\` : ${alias.hint}${dict}.`);
|
|
201
|
+
continue;
|
|
202
|
+
}
|
|
203
|
+
// An endpoint-specific entry without `correct` says "this filter does not
|
|
204
|
+
// exist here" — that hint is always safe to print. A global correction whose
|
|
205
|
+
// replacement this endpoint doesn't accept is not, so it falls through.
|
|
206
|
+
if (alias !== undefined && alias.correct === undefined) {
|
|
207
|
+
lines.push(`Filtre inconnu « ${key} » : ${alias.hint}.`);
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const near = closestKey(key, validKeys);
|
|
211
|
+
lines.push(near !== undefined
|
|
212
|
+
? `Filtre inconnu « ${key} » → vouliez-vous dire \`${near}\` ?`
|
|
213
|
+
: `Filtre inconnu « ${key} » : non supporté par cet endpoint (ne pas le renvoyer).`);
|
|
214
|
+
}
|
|
215
|
+
if (keys.length > explained.length) {
|
|
216
|
+
lines.push(`… et ${keys.length - explained.length} autre(s) filtre(s) inconnu(s) à retirer.`);
|
|
217
|
+
}
|
|
218
|
+
const listed = validKeys.slice(0, MAX_ACCEPTED_KEYS_LISTED);
|
|
219
|
+
const suffix = validKeys.length > listed.length ? `, … (${validKeys.length} au total)` : "";
|
|
220
|
+
if (listed.length > 0) {
|
|
221
|
+
lines.push(`Filtres acceptés : ${listed.join(", ")}${suffix}.`);
|
|
222
|
+
}
|
|
223
|
+
return lines.join("\n");
|
|
224
|
+
}
|
|
225
|
+
//# sourceMappingURL=filter-aliases.js.map
|