boondmanager-mcp-server 2.16.0 → 2.17.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 +36 -4
- package/dist/config/access-policy.d.ts.map +1 -1
- package/dist/config/access-policy.js +6 -18
- package/dist/config/access-policy.js.map +1 -1
- package/dist/config/dictionary-overrides.d.ts.map +1 -1
- package/dist/config/dictionary-overrides.js +2 -12
- package/dist/config/dictionary-overrides.js.map +1 -1
- package/dist/config/env.d.ts +63 -0
- package/dist/config/env.d.ts.map +1 -0
- package/dist/config/env.js +116 -0
- package/dist/config/env.js.map +1 -0
- package/dist/config/profiles.d.ts +22 -8
- package/dist/config/profiles.d.ts.map +1 -1
- package/dist/config/profiles.js +48 -4
- package/dist/config/profiles.js.map +1 -1
- package/dist/constants.d.ts +13 -2
- package/dist/constants.d.ts.map +1 -1
- package/dist/constants.js +25 -1
- package/dist/constants.js.map +1 -1
- package/dist/icons.d.ts.map +1 -1
- package/dist/icons.js +6 -1
- package/dist/icons.js.map +1 -1
- package/dist/index.js +33 -15
- package/dist/index.js.map +1 -1
- package/dist/instructions.d.ts +1 -1
- package/dist/instructions.d.ts.map +1 -1
- package/dist/instructions.js +6 -6
- package/dist/instructions.js.map +1 -1
- package/dist/prompts/index.d.ts +19 -1
- package/dist/prompts/index.d.ts.map +1 -1
- package/dist/prompts/index.js +495 -25
- package/dist/prompts/index.js.map +1 -1
- package/dist/prompts/periods.d.ts +45 -0
- package/dist/prompts/periods.d.ts.map +1 -0
- package/dist/prompts/periods.js +154 -0
- package/dist/prompts/periods.js.map +1 -0
- package/dist/resources/index.d.ts +14 -0
- package/dist/resources/index.d.ts.map +1 -1
- package/dist/resources/index.js +285 -0
- package/dist/resources/index.js.map +1 -1
- package/dist/resources/templates.d.ts +1 -1
- package/dist/resources/templates.d.ts.map +1 -1
- package/dist/resources/templates.js +2 -0
- package/dist/resources/templates.js.map +1 -1
- package/dist/schema-dialect.js.map +1 -1
- package/dist/schemas/filter-aliases.d.ts +1 -1
- package/dist/schemas/filter-aliases.d.ts.map +1 -1
- package/dist/schemas/filter-aliases.js +101 -2
- package/dist/schemas/filter-aliases.js.map +1 -1
- package/dist/schemas/index.d.ts +987 -161
- package/dist/schemas/index.d.ts.map +1 -1
- package/dist/schemas/index.js +853 -323
- package/dist/schemas/index.js.map +1 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +10 -3
- package/dist/server.js.map +1 -1
- package/dist/services/boond-client.d.ts +29 -213
- package/dist/services/boond-client.d.ts.map +1 -1
- package/dist/services/boond-client.js +29 -1168
- package/dist/services/boond-client.js.map +1 -1
- package/dist/services/dictionary.d.ts +27 -3
- package/dist/services/dictionary.d.ts.map +1 -1
- package/dist/services/dictionary.js +60 -23
- package/dist/services/dictionary.js.map +1 -1
- package/dist/services/document-text.d.ts +32 -0
- package/dist/services/document-text.d.ts.map +1 -0
- package/dist/services/document-text.js +120 -0
- package/dist/services/document-text.js.map +1 -0
- package/dist/services/format/detail.d.ts +16 -0
- package/dist/services/format/detail.d.ts.map +1 -0
- package/dist/services/format/detail.js +36 -0
- package/dist/services/format/detail.js.map +1 -0
- package/dist/services/format/html.d.ts +19 -0
- package/dist/services/format/html.d.ts.map +1 -0
- package/dist/services/format/html.js +87 -0
- package/dist/services/format/html.js.map +1 -0
- package/dist/services/format/list.d.ts +3 -0
- package/dist/services/format/list.d.ts.map +1 -0
- package/dist/services/format/list.js +44 -0
- package/dist/services/format/list.js.map +1 -0
- package/dist/services/format/summary.d.ts +16 -0
- package/dist/services/format/summary.d.ts.map +1 -0
- package/dist/services/format/summary.js +172 -0
- package/dist/services/format/summary.js.map +1 -0
- package/dist/services/format/tab.d.ts +9 -0
- package/dist/services/format/tab.d.ts.map +1 -0
- package/dist/services/format/tab.js +32 -0
- package/dist/services/format/tab.js.map +1 -0
- package/dist/services/http/auth.d.ts +44 -0
- package/dist/services/http/auth.d.ts.map +1 -0
- package/dist/services/http/auth.js +146 -0
- package/dist/services/http/auth.js.map +1 -0
- package/dist/services/http/download.d.ts +60 -0
- package/dist/services/http/download.d.ts.map +1 -0
- package/dist/services/http/download.js +160 -0
- package/dist/services/http/download.js.map +1 -0
- package/dist/services/http/errors.d.ts +45 -0
- package/dist/services/http/errors.d.ts.map +1 -0
- package/dist/services/http/errors.js +188 -0
- package/dist/services/http/errors.js.map +1 -0
- package/dist/services/http/rate-limit.d.ts +40 -0
- package/dist/services/http/rate-limit.d.ts.map +1 -0
- package/dist/services/http/rate-limit.js +92 -0
- package/dist/services/http/rate-limit.js.map +1 -0
- package/dist/services/http/retry.d.ts +46 -0
- package/dist/services/http/retry.d.ts.map +1 -0
- package/dist/services/http/retry.js +101 -0
- package/dist/services/http/retry.js.map +1 -0
- package/dist/services/http/transport.d.ts +73 -0
- package/dist/services/http/transport.d.ts.map +1 -0
- package/dist/services/http/transport.js +272 -0
- package/dist/services/http/transport.js.map +1 -0
- package/dist/services/logger.d.ts +53 -2
- package/dist/services/logger.d.ts.map +1 -1
- package/dist/services/logger.js +82 -21
- package/dist/services/logger.js.map +1 -1
- package/dist/services/oauth.d.ts +12 -0
- package/dist/services/oauth.d.ts.map +1 -1
- package/dist/services/oauth.js +21 -8
- package/dist/services/oauth.js.map +1 -1
- package/dist/services/rate-limiter.d.ts +9 -2
- package/dist/services/rate-limiter.d.ts.map +1 -1
- package/dist/services/rate-limiter.js +18 -6
- package/dist/services/rate-limiter.js.map +1 -1
- package/dist/services/request-context.d.ts +47 -0
- package/dist/services/request-context.d.ts.map +1 -0
- package/dist/services/request-context.js +112 -0
- package/dist/services/request-context.js.map +1 -0
- package/dist/services/search.d.ts +26 -0
- package/dist/services/search.d.ts.map +1 -0
- package/dist/services/search.js +98 -0
- package/dist/services/search.js.map +1 -0
- package/dist/services/update-checker.d.ts.map +1 -1
- package/dist/services/update-checker.js +15 -8
- package/dist/services/update-checker.js.map +1 -1
- package/dist/tools/absences.d.ts +9 -0
- package/dist/tools/absences.d.ts.map +1 -1
- package/dist/tools/absences.js +65 -20
- package/dist/tools/absences.js.map +1 -1
- package/dist/tools/actions.d.ts.map +1 -1
- package/dist/tools/actions.js +11 -27
- package/dist/tools/actions.js.map +1 -1
- package/dist/tools/advantages.d.ts +2 -0
- package/dist/tools/advantages.d.ts.map +1 -1
- package/dist/tools/advantages.js +66 -13
- package/dist/tools/advantages.js.map +1 -1
- package/dist/tools/alerts.d.ts +18 -0
- package/dist/tools/alerts.d.ts.map +1 -0
- package/dist/tools/alerts.js +82 -0
- package/dist/tools/alerts.js.map +1 -0
- package/dist/tools/application.d.ts +2 -1
- package/dist/tools/application.d.ts.map +1 -1
- package/dist/tools/application.js +9 -3
- package/dist/tools/application.js.map +1 -1
- package/dist/tools/contacts.d.ts.map +1 -1
- package/dist/tools/contacts.js +3 -7
- package/dist/tools/contacts.js.map +1 -1
- package/dist/tools/contracts.d.ts +22 -0
- package/dist/tools/contracts.d.ts.map +1 -1
- package/dist/tools/contracts.js +185 -52
- package/dist/tools/contracts.js.map +1 -1
- package/dist/tools/crud-factory.d.ts +33 -2
- package/dist/tools/crud-factory.d.ts.map +1 -1
- package/dist/tools/crud-factory.js +42 -15
- package/dist/tools/crud-factory.js.map +1 -1
- package/dist/tools/deliveries.d.ts +3 -0
- package/dist/tools/deliveries.d.ts.map +1 -1
- package/dist/tools/deliveries.js +50 -94
- package/dist/tools/deliveries.js.map +1 -1
- package/dist/tools/description-builders.d.ts +0 -1
- package/dist/tools/description-builders.d.ts.map +1 -1
- package/dist/tools/description-builders.js +46 -3
- package/dist/tools/description-builders.js.map +1 -1
- package/dist/tools/documents.d.ts.map +1 -1
- package/dist/tools/documents.js +73 -21
- package/dist/tools/documents.js.map +1 -1
- package/dist/tools/expenses.d.ts.map +1 -1
- package/dist/tools/expenses.js +2 -11
- package/dist/tools/expenses.js.map +1 -1
- package/dist/tools/find.d.ts +61 -0
- package/dist/tools/find.d.ts.map +1 -0
- package/dist/tools/find.js +221 -0
- package/dist/tools/find.js.map +1 -0
- package/dist/tools/flags.d.ts +5 -0
- package/dist/tools/flags.d.ts.map +1 -1
- package/dist/tools/flags.js +116 -1
- package/dist/tools/flags.js.map +1 -1
- package/dist/tools/forms.d.ts +5 -0
- package/dist/tools/forms.d.ts.map +1 -0
- package/dist/tools/forms.js +64 -0
- package/dist/tools/forms.js.map +1 -0
- package/dist/tools/groupments.d.ts +5 -0
- package/dist/tools/groupments.d.ts.map +1 -0
- package/dist/tools/groupments.js +68 -0
- package/dist/tools/groupments.js.map +1 -0
- package/dist/tools/inactivities.d.ts +5 -0
- package/dist/tools/inactivities.d.ts.map +1 -0
- package/dist/tools/inactivities.js +59 -0
- package/dist/tools/inactivities.js.map +1 -0
- package/dist/tools/index.d.ts +4 -0
- package/dist/tools/index.d.ts.map +1 -1
- package/dist/tools/index.js +4 -0
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/invoices.d.ts.map +1 -1
- package/dist/tools/invoices.js +27 -2
- package/dist/tools/invoices.js.map +1 -1
- package/dist/tools/linked-entity-filters.d.ts +48 -0
- package/dist/tools/linked-entity-filters.d.ts.map +1 -0
- package/dist/tools/linked-entity-filters.js +64 -0
- package/dist/tools/linked-entity-filters.js.map +1 -0
- package/dist/tools/notifications.d.ts.map +1 -1
- package/dist/tools/notifications.js +0 -5
- package/dist/tools/notifications.js.map +1 -1
- package/dist/tools/orders.d.ts.map +1 -1
- package/dist/tools/orders.js +33 -4
- package/dist/tools/orders.js.map +1 -1
- package/dist/tools/parameter-disclosure.js.map +1 -1
- package/dist/tools/payments.d.ts +3 -0
- package/dist/tools/payments.d.ts.map +1 -1
- package/dist/tools/payments.js +43 -109
- package/dist/tools/payments.js.map +1 -1
- package/dist/tools/positionings.d.ts.map +1 -1
- package/dist/tools/positionings.js +9 -39
- package/dist/tools/positionings.js.map +1 -1
- package/dist/tools/projects.d.ts.map +1 -1
- package/dist/tools/projects.js +5 -12
- package/dist/tools/projects.js.map +1 -1
- package/dist/tools/provider-invoices.d.ts +2 -0
- package/dist/tools/provider-invoices.d.ts.map +1 -1
- package/dist/tools/provider-invoices.js +30 -111
- package/dist/tools/provider-invoices.js.map +1 -1
- package/dist/tools/purchases.d.ts +2 -0
- package/dist/tools/purchases.d.ts.map +1 -1
- package/dist/tools/purchases.js +50 -104
- package/dist/tools/purchases.js.map +1 -1
- package/dist/tools/registration-decorators.d.ts +28 -0
- package/dist/tools/registration-decorators.d.ts.map +1 -1
- package/dist/tools/registration-decorators.js +107 -0
- package/dist/tools/registration-decorators.js.map +1 -1
- package/dist/tools/resources.d.ts.map +1 -1
- package/dist/tools/resources.js +27 -5
- package/dist/tools/resources.js.map +1 -1
- package/dist/tools/rights.d.ts +101 -0
- package/dist/tools/rights.d.ts.map +1 -0
- package/dist/tools/rights.js +75 -0
- package/dist/tools/rights.js.map +1 -0
- package/dist/tools/tab-tools.js +3 -3
- package/dist/tools/tab-tools.js.map +1 -1
- package/dist/tools/timesheets.d.ts +29 -0
- package/dist/tools/timesheets.d.ts.map +1 -1
- package/dist/tools/timesheets.js +196 -126
- package/dist/tools/timesheets.js.map +1 -1
- package/dist/tools/todolists.d.ts +2 -0
- package/dist/tools/todolists.d.ts.map +1 -1
- package/dist/tools/todolists.js +64 -1
- package/dist/tools/todolists.js.map +1 -1
- package/dist/tools/validations.d.ts +9 -0
- package/dist/tools/validations.d.ts.map +1 -1
- package/dist/tools/validations.js +86 -9
- package/dist/tools/validations.js.map +1 -1
- package/dist/tools/workflows.js +1 -1
- package/dist/tools/workflows.js.map +1 -1
- package/dist/transports/http.d.ts +116 -0
- package/dist/transports/http.d.ts.map +1 -1
- package/dist/transports/http.js +422 -92
- package/dist/transports/http.js.map +1 -1
- package/dist/types.d.ts +3 -3
- package/dist/types.d.ts.map +1 -1
- package/manifest.json +2 -2
- package/package.json +6 -5
package/dist/prompts/index.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
|
|
1
2
|
import { z } from "zod";
|
|
2
3
|
import { isDomainAllowed } from "../config/access-policy.js";
|
|
4
|
+
import { apiRequest } from "../services/boond-client.js";
|
|
5
|
+
import { addDays, periodBounds, toIsoDate } from "./periods.js";
|
|
3
6
|
/**
|
|
4
7
|
* Pre-orchestrated MCP prompts for the most common Boond workflows.
|
|
5
8
|
*
|
|
@@ -18,12 +21,24 @@ import { isDomainAllowed } from "../config/access-policy.js";
|
|
|
18
21
|
const userMessage = (text) => ({
|
|
19
22
|
messages: [{ role: "user", content: { type: "text", text } }],
|
|
20
23
|
});
|
|
24
|
+
/**
|
|
25
|
+
* One line telling the model the resolved bounds, or — when the caller's
|
|
26
|
+
* wording was not understood — today's date so it at least has an anchor.
|
|
27
|
+
*/
|
|
28
|
+
function periodLine(bounds, rawArg, now) {
|
|
29
|
+
if (bounds) {
|
|
30
|
+
return `Période : ${bounds.label} — du ${bounds.startDate} au ${bounds.endDate} (mois ${bounds.startMonth}${bounds.endMonth !== bounds.startMonth ? ` à ${bounds.endMonth}` : ""}). Utiliser ces dates telles quelles.`;
|
|
31
|
+
}
|
|
32
|
+
return `Période : « ${rawArg?.trim()} » — à interpréter à partir d'aujourd'hui (${toIsoDate(now)}) en dates YYYY-MM-DD avant tout appel.`;
|
|
33
|
+
}
|
|
21
34
|
const SEARCH_TOOL_BY_KIND = {
|
|
22
35
|
resource: "boond_resources_search",
|
|
23
36
|
society: "boond_companies_search",
|
|
24
37
|
opportunity: "boond_opportunities_search",
|
|
25
38
|
agency: "boond_agencies_search",
|
|
26
39
|
project: "boond_projects_search",
|
|
40
|
+
candidate: "boond_candidates_search",
|
|
41
|
+
contact: "boond_contacts_search",
|
|
27
42
|
};
|
|
28
43
|
const LABEL_BY_KIND = {
|
|
29
44
|
resource: "ressource",
|
|
@@ -31,6 +46,8 @@ const LABEL_BY_KIND = {
|
|
|
31
46
|
opportunity: "opportunité",
|
|
32
47
|
agency: "agence",
|
|
33
48
|
project: "projet",
|
|
49
|
+
candidate: "candidat",
|
|
50
|
+
contact: "contact",
|
|
34
51
|
};
|
|
35
52
|
const resolveEntity = (input, kind, placeholder) => {
|
|
36
53
|
const trimmed = input.trim();
|
|
@@ -53,6 +70,29 @@ const ID_OR_NAME_HINT_SOCIETY = "Accepte soit l'ID numérique, soit le nom de la
|
|
|
53
70
|
const ID_OR_NAME_HINT_OPPORTUNITY = "Accepte soit l'ID numérique, soit l'intitulé de l'opportunité (résolution auto via `boond_opportunities_search`).";
|
|
54
71
|
const ID_OR_NAME_HINT_AGENCY = "Accepte soit l'ID numérique, soit le nom de l'agence (résolution auto via `boond_agencies_search`).";
|
|
55
72
|
const ID_OR_NAME_HINT_PROJECT = "Accepte soit l'ID numérique, soit le libellé du projet (résolution auto via `boond_projects_search`).";
|
|
73
|
+
const ID_OR_NAME_HINT_CANDIDATE = "Accepte soit l'ID numérique, soit « Prénom Nom » du candidat (résolution auto via `boond_candidates_search`).";
|
|
74
|
+
/** Manager scope shared by the team-level runbooks: explicit `perimeterManagers` or the caller's own N-1. */
|
|
75
|
+
function managerScope(manager_id) {
|
|
76
|
+
if (manager_id) {
|
|
77
|
+
const r = resolveEntity(manager_id, "resource", "<MANAGER_ID>");
|
|
78
|
+
return { scope: `\`perimeterManagers: [${r.idForFilter}]\``, preamble: r.preamble };
|
|
79
|
+
}
|
|
80
|
+
return { scope: "`perimeterDynamic: ['managers']`", preamble: "" };
|
|
81
|
+
}
|
|
82
|
+
function positiveInt(raw, fallback) {
|
|
83
|
+
const parsed = Number.parseInt(raw ?? "", 10);
|
|
84
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
|
|
85
|
+
}
|
|
86
|
+
const MANAGER_ARG = (what) => z
|
|
87
|
+
.string()
|
|
88
|
+
.optional()
|
|
89
|
+
.describe(`Manager pour restreindre à son équipe (${what}). ` +
|
|
90
|
+
ID_OR_NAME_HINT_RESOURCE +
|
|
91
|
+
" Si absent, scope = mon équipe via `perimeterDynamic: ['managers']`.");
|
|
92
|
+
const MONTH_ARG = (what) => z
|
|
93
|
+
.string()
|
|
94
|
+
.optional()
|
|
95
|
+
.describe(`${what} : \`YYYY-MM\`, « ce mois », « mois dernier », « avril 2026 »… Défaut : le mois en cours.`);
|
|
56
96
|
export const PROMPTS = [
|
|
57
97
|
{
|
|
58
98
|
name: "synthese_equipe",
|
|
@@ -69,11 +109,12 @@ export const PROMPTS = [
|
|
|
69
109
|
periode: z
|
|
70
110
|
.string()
|
|
71
111
|
.optional()
|
|
72
|
-
.describe("Période d'analyse
|
|
112
|
+
.describe("Période d'analyse (ex: 'cette semaine', 'mois dernier', '2026-04', 'avril 2026', '2026-W14'). Défaut: mois en cours. Résolue en dates ISO côté serveur."),
|
|
73
113
|
},
|
|
74
114
|
domains: ["resources", "application"],
|
|
75
|
-
build: ({ manager_id, periode }) => {
|
|
76
|
-
const
|
|
115
|
+
build: ({ manager_id, periode }, now = new Date()) => {
|
|
116
|
+
const bounds = periodBounds(periode, now, "month");
|
|
117
|
+
const periodeText = bounds ? bounds.label : periode.trim();
|
|
77
118
|
let preamble = "";
|
|
78
119
|
let managerStep;
|
|
79
120
|
let managerIdLit;
|
|
@@ -91,7 +132,7 @@ export const PROMPTS = [
|
|
|
91
132
|
const lines = [`Produis une synthèse de l'équipe pour ${periodeText}.`, ""];
|
|
92
133
|
if (preamble)
|
|
93
134
|
lines.push(preamble);
|
|
94
|
-
lines.push(managerStep, "", "Étapes :", `1. Lister les membres de l'équipe : \`boond_resources_search\` avec \`perimeterManagers: [${managerIdLit}]\` et \`resourceStates\` pour ne garder que les actifs (récupère les états valides via \`boond_application_dictionary\` avec \`setting.state.resource\` si besoin).`, "2. Pour chaque ressource retournée, récupérer en parallèle :", " - `boond_resources_positionings` (qui est sur quel projet)", " - `boond_resources_absences_reports` (absences validées/à venir)", " - `boond_resources_times_reports` (CRA
|
|
135
|
+
lines.push(managerStep, periodLine(bounds, periode, now), "", "Étapes :", `1. Lister les membres de l'équipe : \`boond_resources_search\` avec \`perimeterManagers: [${managerIdLit}]\` et \`resourceStates\` pour ne garder que les actifs (récupère les états valides via \`boond_application_dictionary\` avec \`setting.state.resource\` si besoin).`, "2. Pour chaque ressource retournée, récupérer en parallèle :", " - `boond_resources_positionings` (qui est sur quel projet)", " - `boond_resources_absences_reports` (absences validées/à venir — ne retenir que celles qui recoupent la période ci-dessus)", " - `boond_resources_times_reports` (CRA de la période, pour confirmer l'occupation)", "3. Synthétiser un tableau par personne : nom, projet courant, % occupation, absences sur la période, disponibilité.", "4. Conclure par les signaux faibles (sur-/sous-charge, absences non couvertes, ressources sans positionnement).");
|
|
95
136
|
return lines.join("\n");
|
|
96
137
|
},
|
|
97
138
|
},
|
|
@@ -141,7 +182,8 @@ export const PROMPTS = [
|
|
|
141
182
|
.describe("Société ciblée pour la relance. " + ID_OR_NAME_HINT_SOCIETY),
|
|
142
183
|
},
|
|
143
184
|
domains: ["invoices", "application"],
|
|
144
|
-
build: ({ society_id }) => {
|
|
185
|
+
build: ({ society_id }, now = new Date()) => {
|
|
186
|
+
const today = toIsoDate(now);
|
|
145
187
|
let preamble = "";
|
|
146
188
|
let filterLine;
|
|
147
189
|
if (society_id) {
|
|
@@ -152,10 +194,15 @@ export const PROMPTS = [
|
|
|
152
194
|
else {
|
|
153
195
|
filterLine = " - sans filtre société (toutes les factures du périmètre courant)";
|
|
154
196
|
}
|
|
155
|
-
const lines = [
|
|
197
|
+
const lines = [
|
|
198
|
+
`Identifie les factures à relancer (impayées dont l'échéance est dépassée au ${today}).`,
|
|
199
|
+
"",
|
|
200
|
+
];
|
|
156
201
|
if (preamble)
|
|
157
202
|
lines.push(preamble);
|
|
158
|
-
lines.push(
|
|
203
|
+
lines.push(`Date de référence : aujourd'hui = ${today}.`, "", "Étapes :", "1. Lire la ressource `boond://dictionary/states/invoices` (pas d'appel d'outil) et retenir les IDs des états **impayés** : tous les états sauf « payée » (et « annulée » / avoir s'ils existent).", "2. Appeler `boond_invoices_search` **en filtrant côté API** :", filterLine, " - `states: [<IDs impayés de l'étape 1>]`", ' - `period: "expectedPayment"`, `endDate: "' +
|
|
204
|
+
today +
|
|
205
|
+
"\"` — échéance de paiement attendue avant aujourd'hui", " - `creditNote: false`", " - `pageSize: 100`, `fields: ['reference', 'state', 'expectedPaymentDate', 'turnoverInvoicedExcludingTax', 'company']`", " - **Paginer jusqu'au bout** : tant que `structuredContent.count` vaut 100, rappeler avec `page: 2`, `page: 3`… Ne pas s'arrêter à la première page.", `3. Contrôle : chaque facture rendue doit avoir un état de l'étape 1 et une \`expectedPaymentDate\` strictement antérieure au ${today} — sinon signaler l'écart plutôt que de trier à la main.`, "4. Pour chaque facture retenue, récupérer le détail via `boond_invoices_get` si nécessaire pour obtenir le contact/email de relance.", "5. Restituer un tableau groupé par société : société | nombre de factures impayées | total HT impayé | facture la plus ancienne (référence + jours de retard) | contact à relancer.", "6. Ajouter une ligne « Total » en bas.");
|
|
159
206
|
return lines.join("\n");
|
|
160
207
|
},
|
|
161
208
|
},
|
|
@@ -252,7 +299,7 @@ export const PROMPTS = [
|
|
|
252
299
|
horizon_jours: z
|
|
253
300
|
.string()
|
|
254
301
|
.optional()
|
|
255
|
-
.describe("Nombre de jours à anticiper (défaut: 60). Ex: '30' pour ne voir que les fins très proches."),
|
|
302
|
+
.describe("Nombre de jours à anticiper — entier (défaut: 60). Ex: '30' pour ne voir que les fins très proches."),
|
|
256
303
|
manager_id: z
|
|
257
304
|
.string()
|
|
258
305
|
.optional()
|
|
@@ -261,8 +308,12 @@ export const PROMPTS = [
|
|
|
261
308
|
" Si absent, scope = mon équipe via `perimeterDynamic: ['managers']`."),
|
|
262
309
|
},
|
|
263
310
|
domains: ["resources", "application"],
|
|
264
|
-
build: ({ horizon_jours, manager_id }) => {
|
|
265
|
-
const
|
|
311
|
+
build: ({ horizon_jours, manager_id }, now = new Date()) => {
|
|
312
|
+
const parsedHorizon = Number.parseInt(horizon_jours ?? "", 10);
|
|
313
|
+
const horizonDays = Number.isFinite(parsedHorizon) && parsedHorizon > 0 ? parsedHorizon : 60;
|
|
314
|
+
const horizon = String(horizonDays);
|
|
315
|
+
const startDate = toIsoDate(now);
|
|
316
|
+
const endDate = toIsoDate(addDays(now, horizonDays));
|
|
266
317
|
let preamble = "";
|
|
267
318
|
let scope;
|
|
268
319
|
if (manager_id) {
|
|
@@ -279,7 +330,7 @@ export const PROMPTS = [
|
|
|
279
330
|
];
|
|
280
331
|
if (preamble)
|
|
281
332
|
lines.push(preamble);
|
|
282
|
-
lines.push(`Périmètre : ${scope}.`, "", "Étapes :", `1.
|
|
333
|
+
lines.push(`Périmètre : ${scope}.`, "", "Étapes :", `1. Fenêtre (calculée côté serveur) : D = ${startDate} → D+H = ${endDate}. Utiliser ces dates telles quelles.`, "2. Appeler `boond_resources_search` avec :", ` - \`period: "available"\`, \`startDate: "${startDate}"\`, \`endDate: "${endDate}"\``, ` - ${scope}`, " - `resourceStates` filtrés sur les états « actif » (consulter `setting.state.resource`)", ' - `pageSize: 100`, `sort: "availability"`, `order: "asc"`', " → Cela retourne les ressources qui (re)deviennent disponibles dans la fenêtre — proxy direct pour « fin de mission ».", "3. Pour chaque ressource, en parallèle :", " - `boond_resources_positionings` — récupérer le positionnement courant (projet + société + date de fin) et détecter d'éventuels positionnements de relais déjà actés", " - `boond_resources_projects` — historique projet récent pour contexte", "4. Restituer un tableau trié par date de fin croissante : ressource | projet courant | société cliente | date de fin | jours restants | manager | relais déjà identifié (oui/non).", "5. Mettre en évidence en haut du tableau :", " - **Urgent** : fin dans <= 15 jours sans relais identifié", " - **À surveiller** : fin entre 15 et 30 jours sans relais", "6. Conclure par 3-5 actions concrètes : relances commerciales, repositionnements internes, etc.");
|
|
283
334
|
return lines.join("\n");
|
|
284
335
|
},
|
|
285
336
|
},
|
|
@@ -299,7 +350,10 @@ export const PROMPTS = [
|
|
|
299
350
|
.string()
|
|
300
351
|
.optional()
|
|
301
352
|
.describe("Agence pour cartographier toute une agence (alternatif à `manager_id`). " + ID_OR_NAME_HINT_AGENCY),
|
|
302
|
-
top_n: z
|
|
353
|
+
top_n: z
|
|
354
|
+
.string()
|
|
355
|
+
.optional()
|
|
356
|
+
.describe("Nombre de compétences à mettre en avant dans le top — entier (défaut: 20)."),
|
|
303
357
|
},
|
|
304
358
|
domains: ["resources", "opportunities", "application"],
|
|
305
359
|
build: ({ manager_id, agency_id, top_n }) => {
|
|
@@ -335,7 +389,7 @@ export const PROMPTS = [
|
|
|
335
389
|
seuil_mois: z
|
|
336
390
|
.string()
|
|
337
391
|
.optional()
|
|
338
|
-
.describe("Un dossier technique non touché depuis plus de N mois est considéré obsolète (défaut: 12)."),
|
|
392
|
+
.describe("Un dossier technique non touché depuis plus de N mois est considéré obsolète — entier (défaut: 12)."),
|
|
339
393
|
manager_id: z
|
|
340
394
|
.string()
|
|
341
395
|
.optional()
|
|
@@ -433,25 +487,34 @@ export const PROMPTS = [
|
|
|
433
487
|
semaine: z
|
|
434
488
|
.string()
|
|
435
489
|
.optional()
|
|
436
|
-
.describe("Semaine ciblée (ex: 'cette semaine', '
|
|
490
|
+
.describe("Semaine ciblée (ex: 'cette semaine', 'semaine dernière', 'semaine prochaine', '2026-W14'). Défaut: cette semaine. Résolue en dates ISO côté serveur."),
|
|
437
491
|
},
|
|
438
|
-
domains: ["resources", "opportunities", "projects", "application"],
|
|
439
|
-
build: ({ semaine }) => {
|
|
440
|
-
const
|
|
492
|
+
domains: ["resources", "opportunities", "projects", "absences", "timesheets", "application"],
|
|
493
|
+
build: ({ semaine }, now = new Date()) => {
|
|
494
|
+
const bounds = periodBounds(semaine, now, "week");
|
|
495
|
+
const semaineText = bounds ? bounds.label : semaine.trim();
|
|
496
|
+
const start = bounds ? `"${bounds.startDate}"` : "<DEBUT>";
|
|
497
|
+
const end = bounds ? `"${bounds.endDate}"` : "<FIN>";
|
|
498
|
+
const startMonth = bounds ? `"${bounds.startMonth}"` : "<MOIS_DEBUT>";
|
|
499
|
+
const endMonth = bounds ? `"${bounds.endMonth}"` : "<MOIS_FIN>";
|
|
441
500
|
return [
|
|
442
501
|
`Produis mon récap pour ${semaineText}.`,
|
|
443
502
|
"",
|
|
503
|
+
periodLine(bounds, semaine, now),
|
|
504
|
+
"",
|
|
444
505
|
"Étapes :",
|
|
445
506
|
"1. `boond_application_current_user` pour récupérer mon ID.",
|
|
446
|
-
"2.
|
|
447
|
-
"
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
"4.
|
|
507
|
+
"2. `boond_resources_search` avec `perimeterDynamic: ['managers']`, `pageSize: 100`, `fields: ['firstName', 'lastName']` — mon équipe (mes N-1). Conserver la liste des IDs.",
|
|
508
|
+
"3. En parallèle, un seul appel par source (pas un appel par membre) :",
|
|
509
|
+
` a. \`boond_opportunities_search\` avec \`perimeterDynamic: ['data']\`, \`period: 'updated'\`, \`startDate: ${start}\`, \`endDate: ${end}\` — opportunités touchées sur la semaine.`,
|
|
510
|
+
` b. \`boond_absences_search\` avec \`startMonth: ${startMonth}\`, \`endMonth: ${endMonth}\`, \`pageSize: 200\` — toutes les absences du périmètre sur ce(s) mois ; ne retenir que celles dont la ressource est dans l'équipe de l'étape 2 et qui recoupent la semaine.`,
|
|
511
|
+
` c. \`boond_timesheets_search\` avec \`startMonth: ${startMonth}\`, \`endMonth: ${endMonth}\`, \`pageSize: 200\` — les CRA du périmètre sur ce(s) mois. Pour chaque membre de l'équipe : signaler un CRA **absent** sur le mois ; sinon reporter son \`state\` tel quel (le dictionnaire n'a pas de table d'états CRA — ne pas inventer de libellé).`,
|
|
512
|
+
` d. \`boond_projects_search\` avec \`perimeterDynamic: ['data']\`, \`period: 'running'\`, \`startDate: ${start}\`, \`endDate: ${end}\` — mes projets actifs.`,
|
|
513
|
+
"4. Lire `boond://dictionary/states/opportunities` et `boond://dictionary/states/projects` pour libeller les états.",
|
|
514
|
+
"5. Restituer en 5 sections :",
|
|
453
515
|
" - **Pipeline** : opps qui ont bougé (nouvelles, état changé, closing imminent)",
|
|
454
516
|
" - **Équipe** : qui est absent cette semaine, qui termine sa mission",
|
|
517
|
+
" - **CRA** : membres dont le CRA du mois est absent ou non validé (à relancer)",
|
|
455
518
|
" - **Projets** : projets actifs, dont ceux qui s'arrêtent ou démarrent dans la semaine",
|
|
456
519
|
" - **Actions à mener** : 3-5 puces concrètes (relances, validations, repositionnements)",
|
|
457
520
|
].join("\n");
|
|
@@ -515,7 +578,414 @@ export const PROMPTS = [
|
|
|
515
578
|
return lines.filter(Boolean).join("\n");
|
|
516
579
|
},
|
|
517
580
|
},
|
|
581
|
+
{
|
|
582
|
+
name: "alertes_contrats",
|
|
583
|
+
title: "Fins de contrat et périodes d'essai à venir",
|
|
584
|
+
description: "Liste les contrats de travail qui se terminent et les périodes d'essai qui expirent dans les prochains jours " +
|
|
585
|
+
"sur un périmètre, pour anticiper renouvellements, ruptures et entretiens de fin de PE.",
|
|
586
|
+
argsSchema: {
|
|
587
|
+
horizon_jours: z
|
|
588
|
+
.string()
|
|
589
|
+
.optional()
|
|
590
|
+
.describe("Nombre de jours à anticiper — entier (défaut: 45). Ex: '30' pour les échéances les plus proches."),
|
|
591
|
+
manager_id: z
|
|
592
|
+
.string()
|
|
593
|
+
.optional()
|
|
594
|
+
.describe("Manager pour restreindre à son équipe. " +
|
|
595
|
+
ID_OR_NAME_HINT_RESOURCE +
|
|
596
|
+
" Si absent, scope = mon équipe via `perimeterDynamic: ['managers']`."),
|
|
597
|
+
},
|
|
598
|
+
domains: ["contracts", "resources", "application"],
|
|
599
|
+
build: ({ horizon_jours, manager_id }, now = new Date()) => {
|
|
600
|
+
const parsedHorizon = Number.parseInt(horizon_jours ?? "", 10);
|
|
601
|
+
const horizonDays = Number.isFinite(parsedHorizon) && parsedHorizon > 0 ? parsedHorizon : 45;
|
|
602
|
+
const startDate = toIsoDate(now);
|
|
603
|
+
const endDate = toIsoDate(addDays(now, horizonDays));
|
|
604
|
+
let preamble = "";
|
|
605
|
+
let scope;
|
|
606
|
+
if (manager_id) {
|
|
607
|
+
const r = resolveEntity(manager_id, "resource", "<MANAGER_ID>");
|
|
608
|
+
preamble = r.preamble;
|
|
609
|
+
scope = `\`perimeterManagers: [${r.idForFilter}]\``;
|
|
610
|
+
}
|
|
611
|
+
else {
|
|
612
|
+
scope = "`perimeterDynamic: ['managers']`";
|
|
613
|
+
}
|
|
614
|
+
const lines = [
|
|
615
|
+
`Liste les fins de contrat et les fins de période d'essai des ${horizonDays} prochains jours.`,
|
|
616
|
+
"",
|
|
617
|
+
];
|
|
618
|
+
if (preamble)
|
|
619
|
+
lines.push(preamble);
|
|
620
|
+
lines.push(`Périmètre : ${scope}. Fenêtre (calculée côté serveur) : ${startDate} → ${endDate}, à utiliser telle quelle.`, "", "Étapes :", "1. Lire `boond://dictionary/typeOf/contracts` (pas d'appel d'outil) pour traduire `typeOf` (CDI, CDD, freelance…), et `boond://dictionary/states/probations` pour `probationState`.", "2. Appeler `boond_contracts_search` — recherche composée, une requête par ressource, donc `pageSize: 100` et paginer si `structuredContent.count` atteint 100 :", ` - ${scope}, \`resourceStates\` limités aux états « actif » (\`boond://dictionary/states/resources\`)`, ` - \`period: "ending"\`, \`startDate: "${startDate}"\`, \`endDate: "${endDate}"\` → les contrats qui se terminent dans la fenêtre`, '3. Rappeler `boond_contracts_search` avec les mêmes filtres et `period: "probationEnding"` → les périodes d\'essai (initiale ou renouvelée) qui expirent dans la fenêtre.', "4. Pour un contrat de l'étape 2 dont le type est CDD / mission : vérifier via `boond_resources_contracts` qu'aucun contrat suivant n'existe déjà (renouvellement saisi).", "5. Restituer deux tableaux triés par échéance croissante :", " - **Fins de contrat** : ressource | type | début | fin | jours restants | contrat suivant déjà saisi (oui/non) | motif de fin s'il est renseigné", " - **Fins de période d'essai** : ressource | type | fin de PE (initiale / renouvelée) | jours restants | état de la PE", "6. Mettre en évidence les échéances à moins de 15 jours, et conclure par les actions à engager (entretien de fin de PE, proposition de renouvellement, préavis).");
|
|
621
|
+
return lines.join("\n");
|
|
622
|
+
},
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
name: "relance_cra",
|
|
626
|
+
title: "Relance des CRA du mois",
|
|
627
|
+
description: "Pour un mois et une équipe : qui n'a pas saisi son CRA, qui l'a saisi sans le soumettre, et quels CRA attendent " +
|
|
628
|
+
"la validation du manager — avec la liste à relancer et les validations à traiter.",
|
|
629
|
+
argsSchema: { mois: MONTH_ARG("Mois du CRA"), manager_id: MANAGER_ARG("les CRA de ses N-1") },
|
|
630
|
+
domains: ["validations", "timesheets", "resources", "application"],
|
|
631
|
+
build: ({ mois, manager_id }, now = new Date()) => {
|
|
632
|
+
const bounds = periodBounds(mois, now, "month");
|
|
633
|
+
const month = bounds ? bounds.startMonth : "<MOIS>";
|
|
634
|
+
const { scope, preamble } = managerScope(manager_id);
|
|
635
|
+
const lines = [`Relance des CRA du mois ${month}.`, ""];
|
|
636
|
+
if (preamble)
|
|
637
|
+
lines.push(preamble);
|
|
638
|
+
lines.push(periodLine(bounds, mois, now), `Périmètre : ${scope}.`, "", "Étapes :", `1. \`boond_resources_search\` avec ${scope}, \`resourceStates\` limités aux états « actif » (\`boond://dictionary/states/resources\`), \`pageSize: 200\` → l'équipe attendue.`, `2. \`boond_timesheets_search\` avec \`startMonth: "${month}"\`, \`endMonth: "${month}"\`, ${scope}, \`pageSize: 200\` → les CRA existants et leur \`state\` (chaîne du workflow : \`savedAndNoValidation\` = saisi non soumis, \`waitingForValidation\`, \`validated\`, \`rejected\`).`, `3. \`boond_validations_search\` avec \`startMonth: "${month}"\`, \`endMonth: "${month}"\`, \`documentTypes: ["timesReport"]\`, \`validationStates: ["waitingForValidation"]\`, ${scope} → les CRA qui attendent ma validation, avec l'ID de **validation** (différent de l'ID du CRA).`, "4. Croiser : membre sans CRA → **à relancer (saisie)** ; CRA `savedAndNoValidation` → **à relancer (soumission)** ; `rejected` → **à corriger** ; `waitingForValidation` → **à valider par moi**.", '5. Pour chaque CRA à valider : relire `boond_timesheets_get` (jours, absences, cohérence avec le planning), puis proposer `boond_validations_update` avec `decision: "validate"` — ou `"reject"` + `reason` — **une décision à la fois, après accord explicite de l\'utilisateur**.', "6. Restituer deux tableaux : à relancer (ressource | situation | dernière mise à jour) et à valider (ressource | ID de validation | jours saisis | décision proposée), puis un message de relance prêt à envoyer.");
|
|
639
|
+
return lines.join("\n");
|
|
640
|
+
},
|
|
641
|
+
},
|
|
642
|
+
{
|
|
643
|
+
name: "absences_a_valider",
|
|
644
|
+
title: "Demandes d'absence à valider",
|
|
645
|
+
description: "Liste les demandes d'absence en attente de validation sur une équipe, les recoupe avec les absences déjà posées " +
|
|
646
|
+
"sur la période, et prépare la décision (validation ou refus motivé) pour chacune.",
|
|
647
|
+
argsSchema: { mois: MONTH_ARG("Mois des absences"), manager_id: MANAGER_ARG("les demandes de ses N-1") },
|
|
648
|
+
domains: ["validations", "absences", "application"],
|
|
649
|
+
build: ({ mois, manager_id }, now = new Date()) => {
|
|
650
|
+
const bounds = periodBounds(mois, now, "month");
|
|
651
|
+
const startMonth = bounds ? bounds.startMonth : "<MOIS_DEBUT>";
|
|
652
|
+
const endMonth = bounds ? bounds.endMonth : "<MOIS_FIN>";
|
|
653
|
+
const { scope, preamble } = managerScope(manager_id);
|
|
654
|
+
const lines = ["Traite les demandes d'absence en attente de validation.", ""];
|
|
655
|
+
if (preamble)
|
|
656
|
+
lines.push(preamble);
|
|
657
|
+
lines.push(periodLine(bounds, mois, now), `Périmètre : ${scope}.`, "", "Étapes :", `1. \`boond_validations_search\` avec \`startMonth: "${startMonth}"\`, \`endMonth: "${endMonth}"\`, \`documentTypes: ["absencesReport"]\`, \`validationStates: ["waitingForValidation"]\`, ${scope}, \`pageSize: 100\` → les demandes en attente (ID de validation + demande liée).`, "2. Pour chaque demande : `boond_absences_get` sur l'ID de la demande (`documentId`) → dates, type, durée, motif.", `3. \`boond_absences_search\` avec \`startMonth: "${startMonth}"\`, \`endMonth: "${endMonth}"\`, \`validationStates: ["validated"]\`, ${scope}, \`pageSize: 200\` → les absences déjà validées de l'équipe, pour repérer les chevauchements (plusieurs personnes absentes le même jour).`, "4. Restituer un tableau : ressource | type | dates | durée | ID de validation | chevauchements dans l'équipe | décision proposée.", '5. **Décider une demande à la fois, après accord explicite** : `boond_validations_update` avec `decision: "validate"`, ou `"reject"` + `reason` (le refus demande une confirmation à l\'utilisateur). Ne jamais enchaîner les décisions sans accord.');
|
|
658
|
+
return lines.join("\n");
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
name: "marge_projet",
|
|
663
|
+
title: "Marge d'un projet : simulé vs réalisé",
|
|
664
|
+
description: "Compare le chiffre d'affaires, les coûts et la marge simulés d'un projet à son réalisé (productivité, reporting), " +
|
|
665
|
+
"prestation par prestation, et explique l'écart.",
|
|
666
|
+
argsSchema: {
|
|
667
|
+
project_id: z.string().describe("Projet à analyser. " + ID_OR_NAME_HINT_PROJECT),
|
|
668
|
+
periode: z
|
|
669
|
+
.string()
|
|
670
|
+
.optional()
|
|
671
|
+
.describe("Fenêtre du reporting : `YYYY-MM-DD..YYYY-MM-DD`, « ce mois », « 2026 »… Défaut : toute la vie du projet."),
|
|
672
|
+
},
|
|
673
|
+
domains: ["projects", "reporting", "application"],
|
|
674
|
+
build: ({ project_id, periode }, now = new Date()) => {
|
|
675
|
+
const r = resolveEntity(project_id ?? "", "project", "<PROJET_ID>");
|
|
676
|
+
const bounds = periode ? periodBounds(periode, now, "month") : null;
|
|
677
|
+
const lines = [`Analyse la marge du projet \`${r.idForFilter}\` : simulé vs réalisé.`, ""];
|
|
678
|
+
if (r.preamble)
|
|
679
|
+
lines.push(r.preamble);
|
|
680
|
+
if (periode)
|
|
681
|
+
lines.push(periodLine(bounds, periode, now));
|
|
682
|
+
const window = bounds
|
|
683
|
+
? `\`startDate: "${bounds.startDate}"\`, \`endDate: "${bounds.endDate}"\``
|
|
684
|
+
: "sans bornes de dates (`startDate` / `endDate` = début et fin du projet lus à l'étape 1)";
|
|
685
|
+
lines.push("", "Étapes :", `1. \`boond_projects_get\` sur \`${r.idForFilter}\` → client, dates, mode (régie / forfait), responsable.`, `2. \`boond_projects_simulation\` → CA, coûts et marge **simulés** ; \`boond_projects_deliveries_groupments\` → les prestations avec TJM (\`averageDailyPriceExcludingTax\`), CJM (\`averageDailyCost\`) et jours prévus.`, `3. \`boond_projects_productivity\` → le **réalisé** par prestation (jours produits, CA, coûts).`, `4. \`boond_reporting_projects\` avec \`projects: [${r.idForFilter}]\`, ${window} → les agrégats BoondManager (CA facturé, coûts, marge, taux de marge) sur la fenêtre.`, "5. Restituer : un tableau par prestation (ressource | TJM | CJM | jours prévus / produits | CA simulé / réalisé | marge simulée / réalisée), puis le total projet et le **taux de marge** — en nommant les écarts (jours non produits, TJM revu, sous-traitance plus chère) et ce qui reste à facturer.", "6. Ne pas recalculer ce que BoondManager fournit : reprendre ses montants, et signaler quand deux sources divergent.");
|
|
686
|
+
return lines.join("\n");
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
name: "preparation_entretien",
|
|
691
|
+
title: "Préparer un entretien candidat",
|
|
692
|
+
description: "Rassemble en une fiche tout ce que BoondManager sait d'un candidat (parcours, compétences, CV, positionnements, " +
|
|
693
|
+
"historique des échanges) et, si une opportunité est visée, confronte le profil au besoin pour lister les points à creuser.",
|
|
694
|
+
argsSchema: {
|
|
695
|
+
candidate_id: z.string().describe("Candidat reçu en entretien. " + ID_OR_NAME_HINT_CANDIDATE),
|
|
696
|
+
opportunity_id: z
|
|
697
|
+
.string()
|
|
698
|
+
.optional()
|
|
699
|
+
.describe("Opportunité visée, pour comparer profil et besoin. " + ID_OR_NAME_HINT_OPPORTUNITY),
|
|
700
|
+
},
|
|
701
|
+
domains: ["candidates", "opportunities", "documents", "application"],
|
|
702
|
+
build: ({ candidate_id, opportunity_id }) => {
|
|
703
|
+
const c = resolveEntity(candidate_id ?? "", "candidate", "<CANDIDAT_ID>");
|
|
704
|
+
const lines = [`Prépare l'entretien du candidat \`${c.idForFilter}\`.`, ""];
|
|
705
|
+
if (c.preamble)
|
|
706
|
+
lines.push(c.preamble);
|
|
707
|
+
let opp;
|
|
708
|
+
if (opportunity_id) {
|
|
709
|
+
opp = resolveEntity(opportunity_id, "opportunity", "<OPPORTUNITE_ID>");
|
|
710
|
+
if (opp.preamble)
|
|
711
|
+
lines.push(opp.preamble);
|
|
712
|
+
}
|
|
713
|
+
lines.push("Étapes :", `1. Lire la ressource \`boond://candidate/${c.idForFilter}\` (fiche + informations + compétences en une lecture) — à défaut \`boond_candidates_get\`, \`boond_candidates_information\` et \`boond_candidates_technical_data\`.`, "2. Le CV : dans les relations `resumes` de la fiche, prendre le document le plus récent et l'ouvrir avec `boond_documents_get` (ID suffixé, ex. `123_resume`). S'il est absent, le dire.", `3. \`boond_candidates_positionings\` → positionnements en cours et passés (opportunités, états, dates) ; \`boond_candidates_actions\` → l'historique des échanges (dernier contact, promesses faites, disponibilité, prétentions).`, opp
|
|
714
|
+
? `4. Le besoin : \`boond_opportunities_get\` sur \`${opp.idForFilter}\` et \`boond_opportunities_information\` → compétences attendues (\`tools\`, \`expertiseAreas\`), expérience, mobilité, TJM cible, dates.`
|
|
715
|
+
: "4. Sans opportunité visée : s'en tenir au profil, et repérer les opportunités ouvertes proches via `boond_opportunities_search` (`keywords` sur les compétences principales) pour orienter l'entretien.", "5. Restituer une **fiche de préparation** : synthèse du profil (5 lignes) | compétences déclarées vs attendues (✓ / ✗ / à vérifier) | parcours et dates à confirmer | disponibilité, mobilité, prétentions connues | historique des échanges | **8 à 10 questions ciblées** (trous du CV, compétences non prouvées, motivation, contraintes).", "6. Ne rien affirmer que la fiche ne dit pas : un champ vide est « non renseigné », pas une supposition.");
|
|
716
|
+
return lines.join("\n");
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
{
|
|
720
|
+
name: "preparation_rdv_client",
|
|
721
|
+
title: "Préparer un rendez-vous client",
|
|
722
|
+
description: "Brief de rendez-vous pour une société cliente : contacts, opportunités en cours, projets et prestations, factures impayées, " +
|
|
723
|
+
"derniers échanges — et les sujets à aborder.",
|
|
724
|
+
argsSchema: {
|
|
725
|
+
society_id: z.string().describe("Société cliente. " + ID_OR_NAME_HINT_SOCIETY),
|
|
726
|
+
horizon_jours: z
|
|
727
|
+
.string()
|
|
728
|
+
.optional()
|
|
729
|
+
.describe("Profondeur de l'historique des échanges en jours — entier (défaut: 90)."),
|
|
730
|
+
},
|
|
731
|
+
domains: ["companies", "contacts", "opportunities", "projects", "invoices", "actions", "application"],
|
|
732
|
+
build: ({ society_id, horizon_jours }, now = new Date()) => {
|
|
733
|
+
const s = resolveEntity(society_id ?? "", "society", "<SOCIETE_ID>");
|
|
734
|
+
const horizon = positiveInt(horizon_jours, 90);
|
|
735
|
+
const since = toIsoDate(addDays(now, -horizon));
|
|
736
|
+
const today = toIsoDate(now);
|
|
737
|
+
const lines = [`Prépare le rendez-vous avec la société \`${s.idForFilter}\`.`, ""];
|
|
738
|
+
if (s.preamble)
|
|
739
|
+
lines.push(s.preamble);
|
|
740
|
+
lines.push(`Date de référence : aujourd'hui = ${today}.`, "", "Étapes :", `1. Lire la ressource \`boond://company/${s.idForFilter}\` (fiche + informations) — à défaut \`boond_companies_get\`.`, `2. \`boond_companies_contacts\` → interlocuteurs (fonction, e-mail, téléphone) ; \`boond_companies_opportunities\` → opportunités et leur état (lire \`boond://dictionary/states/opportunities\` pour les libellés) ; \`boond_companies_projects\` → projets en cours et prestations.`, `3. Factures : lire \`boond://dictionary/states/invoices\` puis \`boond_invoices_search\` avec \`companyId: "${s.idForFilter}"\`, \`states: [<états impayés>]\`, \`period: "expectedPayment"\`, \`endDate: "${today}"\` → les impayés échus à évoquer (ou à ne pas évoquer, selon le contexte).`, `4. \`boond_actions_search\` avec \`companyId: "${s.idForFilter}"\`, \`period: "started"\`, \`startDate: "${since}"\`, \`endDate: "${today}"\`, \`sort: "startDate"\`, \`order: "desc"\`, \`pageSize: 50\` → les ${horizon} derniers jours d'échanges (qui a parlé à qui, engagements pris).`, "5. Restituer le **brief** : la société en 3 lignes | qui on rencontre et qui on connaît | opportunités ouvertes (montant, étape, prochaine action) | projets / consultants en place et fins de prestation proches | impayés (montant, ancienneté) | derniers échanges | **5 sujets à aborder** et les questions à poser.", "6. Proposer, après le rendez-vous, de tracer le compte rendu avec `boond_actions_create` rattaché au contact rencontré (`contactId` + `companyId`).");
|
|
741
|
+
return lines.join("\n");
|
|
742
|
+
},
|
|
743
|
+
},
|
|
744
|
+
{
|
|
745
|
+
name: "relance_devis",
|
|
746
|
+
title: "Devis et propositions à relancer",
|
|
747
|
+
description: "Trouve les opportunités en phase de proposition envoyée / négociation sans action depuis N jours sur un périmètre, " +
|
|
748
|
+
"et prépare les relances.",
|
|
749
|
+
argsSchema: {
|
|
750
|
+
jours_sans_action: z
|
|
751
|
+
.string()
|
|
752
|
+
.optional()
|
|
753
|
+
.describe("Silence minimal en jours pour relancer — entier (défaut: 15)."),
|
|
754
|
+
manager_id: MANAGER_ARG("les opportunités de ses N-1"),
|
|
755
|
+
},
|
|
756
|
+
domains: ["opportunities", "actions", "application"],
|
|
757
|
+
build: ({ jours_sans_action, manager_id }, now = new Date()) => {
|
|
758
|
+
const days = positiveInt(jours_sans_action, 15);
|
|
759
|
+
const cutoff = toIsoDate(addDays(now, -days));
|
|
760
|
+
const today = toIsoDate(now);
|
|
761
|
+
const { scope, preamble } = managerScope(manager_id);
|
|
762
|
+
const lines = [`Liste les propositions commerciales à relancer (aucune action depuis ${days} jours).`, ""];
|
|
763
|
+
if (preamble)
|
|
764
|
+
lines.push(preamble);
|
|
765
|
+
lines.push(`Date de référence : aujourd'hui = ${today} ; seuil = ${cutoff}. Périmètre : ${scope}.`, "", "Étapes :", "1. Lire `boond://dictionary/states/opportunities` (pas d'appel d'outil) et retenir les IDs des états « proposition envoyée », « en négociation », « soutenance » — les états où le client a la balle.", `2. \`boond_opportunities_search\` avec \`opportunityStates: [<IDs de l'étape 1>]\`, ${scope}, \`pageSize: 100\` → les propositions en cours (société, montant, date de clôture prévue).`, `3. Pour chaque opportunité : \`boond_actions_search\` avec \`opportunityId\`, \`period: "started"\`, \`startDate: "${cutoff}"\`, \`endDate: "${today}"\`, \`pageSize: 1\` → aucune ligne = **à relancer** ; sinon noter la dernière action.`, "4. Pour les opportunités à relancer : `boond_opportunities_actions` (dernier échange, promesse faite, interlocuteur) pour rédiger une relance pertinente.", "5. Restituer un tableau trié par montant décroissant : opportunité | société | montant | état | date de clôture | dernier échange | interlocuteur | relance proposée (canal + angle).", "6. Proposer de tracer chaque relance faite avec `boond_actions_create` (`opportunityId`, `typeOf` via `boond://dictionary/actions/opportunities`) — après accord de l'utilisateur, jamais en lot silencieux.");
|
|
766
|
+
return lines.join("\n");
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
{
|
|
770
|
+
name: "purge_rgpd_candidats",
|
|
771
|
+
title: "Purge RGPD des candidats inactifs",
|
|
772
|
+
description: "Identifie les candidats sans mise à jour ni positionnement actif depuis N mois, présente la liste à confirmer, " +
|
|
773
|
+
"puis supprime candidat par candidat avec confirmation — jamais en lot silencieux.",
|
|
774
|
+
argsSchema: {
|
|
775
|
+
mois_inactivite: z
|
|
776
|
+
.string()
|
|
777
|
+
.optional()
|
|
778
|
+
.describe("Ancienneté minimale de la dernière mise à jour, en mois — entier (défaut: 24)."),
|
|
779
|
+
manager_id: MANAGER_ARG("les candidats suivis par ses N-1"),
|
|
780
|
+
},
|
|
781
|
+
domains: ["candidates", "application"],
|
|
782
|
+
build: ({ mois_inactivite, manager_id }, now = new Date()) => {
|
|
783
|
+
const months = positiveInt(mois_inactivite, 24);
|
|
784
|
+
const cutoffDate = new Date(now.getFullYear(), now.getMonth() - months, now.getDate());
|
|
785
|
+
const cutoff = toIsoDate(cutoffDate);
|
|
786
|
+
const { scope, preamble } = managerScope(manager_id);
|
|
787
|
+
const lines = [
|
|
788
|
+
`Purge RGPD : candidats sans activité depuis ${months} mois (dernière mise à jour avant le ${cutoff}).`,
|
|
789
|
+
"",
|
|
790
|
+
];
|
|
791
|
+
if (preamble)
|
|
792
|
+
lines.push(preamble);
|
|
793
|
+
lines.push(`Périmètre : ${scope}.`, "", "Étapes :", `1. \`boond_candidates_search\` avec \`period: "updated"\`, \`startDate: "2000-01-01"\`, \`endDate: "${cutoff}"\`, ${scope}, \`sort: "updateDate"\`, \`order: "asc"\`, \`pageSize: 100\`, \`fields: ["firstName", "lastName", "updateDate", "state"]\` — **paginer jusqu'au bout**.`, "2. Exclure d'office : les candidats dont l'état signifie « en cours de recrutement » ou « embauché » (`boond://dictionary/states/candidates`), et ceux qui ont un positionnement encore ouvert — vérifier avec `boond_candidates_positionings` sur chaque candidat restant en état ambigu.", "3. Restituer la liste à purger : candidat | dernière mise à jour | état | positionnements | motif de conservation éventuel — et **attendre la validation explicite** de l'utilisateur, ligne par ligne ou en bloc, avant toute suppression.", "4. Supprimer avec `boond_candidates_delete`, **un candidat par appel** : l'outil demande une confirmation (elicitation) et un refus annule l'appel (`deleted: false`). Ne pas contourner, ne pas grouper.", "5. Restituer le bilan : supprimés | conservés (avec le motif) | refusés à la confirmation. Rappeler que les documents (CV) rattachés partent avec le candidat et qu'aucune restauration n'est possible côté API.");
|
|
794
|
+
return lines.join("\n");
|
|
795
|
+
},
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
name: "preparation_facturation",
|
|
799
|
+
title: "Préparation de la facturation mensuelle",
|
|
800
|
+
description: "Pour un mois : CRA validés, prestations en cours et commandes concernées, reste à facturer par commande, factures déjà émises — " +
|
|
801
|
+
"et la liste de ce qui bloque (CRA manquants ou non validés).",
|
|
802
|
+
argsSchema: { mois: MONTH_ARG("Mois à facturer"), manager_id: MANAGER_ARG("les projets de ses N-1") },
|
|
803
|
+
domains: ["timesheets", "deliveries", "orders", "invoices", "application"],
|
|
804
|
+
build: ({ mois, manager_id }, now = new Date()) => {
|
|
805
|
+
const bounds = periodBounds(mois, now, "month");
|
|
806
|
+
const month = bounds ? bounds.startMonth : "<MOIS>";
|
|
807
|
+
const startDate = bounds ? bounds.startDate : "<DATE_DEBUT>";
|
|
808
|
+
const endDate = bounds ? bounds.endDate : "<DATE_FIN>";
|
|
809
|
+
const { scope, preamble } = managerScope(manager_id);
|
|
810
|
+
const lines = [`Prépare la facturation du mois ${month}.`, ""];
|
|
811
|
+
if (preamble)
|
|
812
|
+
lines.push(preamble);
|
|
813
|
+
lines.push(periodLine(bounds, mois, now), `Périmètre : ${scope}.`, "", "Étapes :", `1. \`boond_deliveries_search\` avec \`period: "running"\`, \`startDate: "${startDate}"\`, \`endDate: "${endDate}"\`, ${scope}, \`pageSize: 200\` → les prestations actives sur le mois (ressource, projet, TJM, jours prévus).`, `2. \`boond_timesheets_search\` avec \`startMonth: "${month}"\`, \`endMonth: "${month}"\`, ${scope}, \`pageSize: 200\` → les CRA du mois et leur \`state\` ; seuls les \`validated\` sont facturables. Un CRA absent ou non validé sur une prestation active = **bloquant**, à lister.`, `3. \`boond_orders_search\` avec \`period: "period"\`, \`startDate: "${startDate}"\`, \`endDate: "${endDate}"\`, ${scope}, \`pageSize: 200\` → les bons de commande couvrant le mois ; pour chacun, \`boond_orders_invoices\` → les totaux BoondManager : commandé HT, facturé HT, **\`deltaInvoicedExcludingTax\` = reste à facturer**.`, `4. \`boond_invoices_search\` avec \`period: "period"\`, \`startDate: "${startDate}"\`, \`endDate: "${endDate}"\`, ${scope}, \`pageSize: 200\` → les factures déjà émises sur le mois, pour ne rien facturer deux fois.`, "5. Restituer : (a) **à facturer** — commande | client | prestations / ressources | jours validés × TJM | reste à facturer sur la commande | déjà émis ; (b) **bloqué** — prestation | ressource | CRA manquant ou en attente de validation (renvoyer vers `relance_cra`) ; (c) **commandes épuisées** (reste à facturer ≤ 0 alors que des jours sont validés) → avenant ou nouvelle commande à demander.", "6. Ne pas créer les factures ici : `boond_invoices_create` s'utilise ensuite, commande par commande, après validation du tableau par l'utilisateur.");
|
|
814
|
+
return lines.join("\n");
|
|
815
|
+
},
|
|
816
|
+
},
|
|
817
|
+
{
|
|
818
|
+
name: "ingest_communication",
|
|
819
|
+
title: "Ingérer un e-mail ou un compte rendu dans le CRM",
|
|
820
|
+
description: "À partir d'un e-mail, d'un compte rendu d'appel ou d'une note de réunion collés dans la conversation : extrait les " +
|
|
821
|
+
"entités (contact, société, engagements), déduplique contre BoondManager, présente le plan d'écriture, puis crée ou " +
|
|
822
|
+
"rattache contact / société / action après validation explicite. Aucun Sampling : c'est le modèle qui extrait.",
|
|
823
|
+
argsSchema: {
|
|
824
|
+
contenu: z
|
|
825
|
+
.string()
|
|
826
|
+
.optional()
|
|
827
|
+
.describe("Le texte brut (e-mail, CR d'appel, note). Absent → prendre ce qui a été collé dans la conversation."),
|
|
828
|
+
type_action: z
|
|
829
|
+
.string()
|
|
830
|
+
.optional()
|
|
831
|
+
.describe("Nature de l'action à tracer : appel, email, rendez-vous, note… Défaut : déduite du texte."),
|
|
832
|
+
opportunite_id: z
|
|
833
|
+
.string()
|
|
834
|
+
.optional()
|
|
835
|
+
.describe("Opportunité à laquelle rattacher l'action. " + ID_OR_NAME_HINT_OPPORTUNITY),
|
|
836
|
+
},
|
|
837
|
+
domains: ["contacts", "companies", "actions", "application"],
|
|
838
|
+
build: ({ contenu, type_action, opportunite_id }, now = new Date()) => {
|
|
839
|
+
const today = toIsoDate(now);
|
|
840
|
+
const lines = ["Ingère la communication ci-dessous dans BoondManager (contact, société, action).", ""];
|
|
841
|
+
let opp;
|
|
842
|
+
if (opportunite_id) {
|
|
843
|
+
opp = resolveEntity(opportunite_id, "opportunity", "<OPPORTUNITE_ID>");
|
|
844
|
+
if (opp.preamble)
|
|
845
|
+
lines.push(opp.preamble);
|
|
846
|
+
}
|
|
847
|
+
lines.push(contenu
|
|
848
|
+
? `Texte à traiter :\n"""\n${contenu.trim()}\n"""`
|
|
849
|
+
: "Texte à traiter : celui collé dans la conversation (le demander s'il n'y en a pas).", `Date de référence : aujourd'hui = ${today}.${type_action ? ` Type d'action demandé : « ${type_action} ».` : ""}`, "", "Étapes :", "**1. Extraire** — expéditeur (prénom, nom, e-mail, téléphone, fonction), société (nom, domaine de l'e-mail), objet, engagements pris de part et d'autre, échéances, besoins exprimés, date de l'échange. Ne rien inventer : un champ absent reste vide.", "", "**2. Dédupliquer avant toute création** — l'étape la plus importante :", ' - contact : `boond_find` avec `entity: "contact"` et `email` (l\'e-mail extrait) ; à défaut d\'e-mail, `boond_find` avec `query: "Prénom Nom"` — plusieurs correspondances exactes reviennent en erreur avec la liste : demander à l\'utilisateur lequel ;', ' - société : `boond_find` avec `entity: "company"` et `query` = nom de la société (ou `boond_companies_search` avec `keywords` = domaine de l\'e-mail) ;', " - si le contact existe, lire sa société via `boond_contacts_get` et la comparer à celle du texte ;", opp
|
|
850
|
+
? ` - opportunité : \`${opp.idForFilter}\` (fournie).`
|
|
851
|
+
: ' - opportunité : si le texte en désigne une, `boond_find` avec `entity: "opportunity"` ; sinon aucune.', "", "**3. Présenter le plan d'écriture et ATTENDRE la validation.** Un tableau : entité | action (créer / rattacher à #id existant / ignorer) | valeurs. Signaler les champs déduits (fonction devinée, société déduite du domaine). **Aucune écriture avant un « oui » explicite.**", "", "**4. Écrire, dans cet ordre** (le contact référence la société, l'action référence le contact) :", " - société manquante → `boond_companies_create` ;", " - contact manquant → `boond_contacts_create` avec `companyId` ;", ` - action → \`boond_actions_create\` avec \`contactId\` (+ \`companyId\`), \`typeOf\` lu dans \`boond://dictionary/actions/contacts\`${type_action ? ` (type « ${type_action} »)` : " (appel / e-mail / RDV / note selon le texte)"}, \`startDate\` = date de l'échange en ISO 8601 avec fuseau, \`title\` = objet, \`text\` = compte rendu structuré (contexte, engagements, prochaine étape).`, opp ? ` - rattacher l'action à l'opportunité \`${opp.idForFilter}\` (\`opportunityId\`).` : "", "", "**5. Restituer** les IDs créés ou rattachés, et proposer la suite : un rappel (`boond_actions_create` daté de l'échéance) pour chaque engagement pris.");
|
|
852
|
+
return lines.filter(Boolean).join("\n");
|
|
853
|
+
},
|
|
854
|
+
},
|
|
855
|
+
{
|
|
856
|
+
name: "attention_du_jour",
|
|
857
|
+
title: "Qu'est-ce qui demande mon attention aujourd'hui ?",
|
|
858
|
+
description: "Lit les indicateurs d'alerte configurés sur le tableau de bord (fins de contrat, périodes d'essai, CRA / notes / absences non validés, " +
|
|
859
|
+
"actions à venir…), exécute la recherche correspondante avec les seuils configurés, et classe ce qui en sort par urgence.",
|
|
860
|
+
argsSchema: {},
|
|
861
|
+
domains: ["alerts", "application"],
|
|
862
|
+
build: (_args, now = new Date()) => {
|
|
863
|
+
const today = toIsoDate(now);
|
|
864
|
+
return [
|
|
865
|
+
`Fais le point sur ce qui demande mon attention aujourd'hui (${today}).`,
|
|
866
|
+
"",
|
|
867
|
+
"Étapes :",
|
|
868
|
+
"1. Lire la ressource `boond://alerts/me` (pas d'appel d'outil) — à défaut `boond_alerts_search`. Elle liste les **indicateurs configurés** sur mon tableau de bord, avec leurs seuils (`params.period` en jours, `-1` = mois précédent ; `X` / `Y` = IDs d'états ou de types ; `perimeter`, `dynamic_data` = mes données) — pas les occurrences.",
|
|
869
|
+
`2. Pour chaque indicateur, exécuter la recherche correspondante avec ses seuils (aujourd'hui = ${today}) :`,
|
|
870
|
+
' - `contractsEndedUpcoming` → `boond_contracts_search` avec `perimeterDynamic: ["data"]`, `period: "ending"`, `startDate` = aujourd\'hui, `endDate` = aujourd\'hui + `period` jours ;',
|
|
871
|
+
' - `resourcesProbationaryDateUpcoming` → idem avec `period: "probationEnding"` ;',
|
|
872
|
+
' - `timesReportsWithNoValidation` / `expensesReportsWithNoValidation` / `absencesReportsWithNoValidation` → `boond_validations_search` avec `documentTypes` (`timesReport` / `expensesReport` / `absencesReport`), `validationStates: ["waitingForValidation"]`, `startMonth` / `endMonth` = mois précédent quand `period` vaut -1, `perimeterDynamic: ["data"]` ;',
|
|
873
|
+
" - `actionsUpcoming` → `boond_actions_search` avec `period: \"started\"`, `startDate` = aujourd'hui, `endDate` = aujourd'hui + `period` jours, `actionTypes` = `X` si non vide ;",
|
|
874
|
+
" - un indicateur sans correspondance (`resourcesWithFollowedDocuments`…) est listé tel quel, sans invention.",
|
|
875
|
+
"3. Classer ce qui en sort : **urgent** (échéance dépassée ou sous 7 jours), **à traiter cette semaine**, **à surveiller**.",
|
|
876
|
+
"4. Restituer une liste courte, du plus urgent au moins urgent : élément | indicateur | délai | action proposée (`boond_validations_update`, relance, renouvellement…). Terminer par les trois choses à faire en premier.",
|
|
877
|
+
"5. Un indicateur dont la recherche ne renvoie rien est un « rien à signaler », pas un échec.",
|
|
878
|
+
].join("\n");
|
|
879
|
+
},
|
|
880
|
+
},
|
|
881
|
+
{
|
|
882
|
+
name: "saisir_cra",
|
|
883
|
+
title: "Saisir ou compléter un CRA",
|
|
884
|
+
description: "Saisit la feuille de temps (CRA) d'un collaborateur pour un mois : référentiels de saisie, planning prévu, " +
|
|
885
|
+
"vérification d'un CRA existant, récapitulatif validé par l'utilisateur, puis création ou mise à jour.",
|
|
886
|
+
argsSchema: {
|
|
887
|
+
resource_id: z
|
|
888
|
+
.string()
|
|
889
|
+
.optional()
|
|
890
|
+
.describe("Collaborateur concerné. " +
|
|
891
|
+
ID_OR_NAME_HINT_RESOURCE +
|
|
892
|
+
" Si absent, `boond_application_current_user` est appelé pour le récupérer."),
|
|
893
|
+
term: z.string().optional().describe("Mois du CRA (YYYY-MM). Défaut : mois en cours."),
|
|
894
|
+
consignes: z
|
|
895
|
+
.string()
|
|
896
|
+
.optional()
|
|
897
|
+
.describe("Précisions libres (ex: « 2 jours de RTT les 12 et 13 », « demi-journée le 20 sur le projet X »)."),
|
|
898
|
+
},
|
|
899
|
+
domains: ["timesheets", "application"],
|
|
900
|
+
build: ({ resource_id, term, consignes }, now = new Date()) => {
|
|
901
|
+
const bounds = periodBounds(term, now, "month");
|
|
902
|
+
const month = bounds ? bounds.startMonth : (term ?? "").trim();
|
|
903
|
+
const lines = [`Saisis le CRA du mois ${month || "<YYYY-MM>"} dans BoondManager.`, ""];
|
|
904
|
+
let resourceLit = "<RESOURCE_ID>";
|
|
905
|
+
let resourceStep;
|
|
906
|
+
if (resource_id) {
|
|
907
|
+
const r = resolveEntity(resource_id, "resource", "<RESOURCE_ID>");
|
|
908
|
+
if (r.preamble)
|
|
909
|
+
lines.push(r.preamble);
|
|
910
|
+
resourceLit = r.idForFilter;
|
|
911
|
+
resourceStep = `Le collaborateur concerné a pour ID \`${resourceLit}\`.`;
|
|
912
|
+
}
|
|
913
|
+
else {
|
|
914
|
+
resourceStep = "Appeler `boond_application_current_user` pour obtenir mon ID → ce sera `<RESOURCE_ID>`.";
|
|
915
|
+
}
|
|
916
|
+
lines.push(consignes ? `Consignes de l'utilisateur : « ${consignes} ».` : "", consignes ? "" : "", "Étapes :", "", `**1. Identifier le collaborateur et le mois.** ${resourceStep} Le mois est \`term = "${month || "<YYYY-MM>"}"\`${bounds ? ` (du ${bounds.startDate} au ${bounds.endDate})` : ""}.`, "", `**2. Récupérer les référentiels de saisie** : \`boond_timesheets_default\` avec \`resourceId: "${resourceLit}"\` et ce \`term\`. Il retourne :`, " - les **types d'unité d'œuvre** autorisés pour cette ressource (`reference` + libellé + `activityType` : production / absence / exceptionalTime) — ils ne sont **pas** dans `boond_application_dictionary` ;", " - les couples `projectId` / `deliveryId` imputables ce mois-là — obligatoires sur chaque ligne de production ;", " - le **planning prévu** (jours et imputations issus des prestations) et les absences déjà posées.", " - Aucune imputation disponible → seules des absences peuvent être saisies ; le signaler.", "", `**3. Vérifier si un CRA existe déjà** : \`boond_timesheets_search\` avec \`startMonth: "${month || "<YYYY-MM>"}"\`, \`endMonth: "${month || "<YYYY-MM>"}"\`, \`resourceId: "${resourceLit}"\`.`, " - ⚠️ L'API **ne déduplique pas** : vérifier, pas supposer. CRA existant → le relire avec `boond_timesheets_get` (ses lignes actuelles) et le compléter à l'étape 5 via `boond_timesheets_update` ; aucun → `boond_timesheets_create`.", "", "**4. Construire les lignes et ATTENDRE la validation de l'utilisateur.** Partir du planning prévu, appliquer les consignes (absences, demi-journées, jours non travaillés), une ligne par jour : `startDate`, `duration` (1 = journée, 0.5 = demi-journée), `workUnitTypeReference`, et pour la production `projectId` + `deliveryId`.", " - Présenter un tableau : jour | durée | type (libellé + `reference`) | projet / prestation. Puis le total du mois. Ne pas écrire avant un « oui » explicite.", " - Ne jamais inventer une imputation : un couple hors de la liste de l'étape 2 est refusé par l'API.", "", "**5. Écrire.** Nouveau CRA → `boond_timesheets_create` (`resourceId`, `term`, `regularTimes[]`). CRA existant → `boond_timesheets_update` avec la liste **complète** des lignes (les lignes existantes fusionnées avec les nouvelles) : `regularTimes` remplace tout le tableau, une liste partielle efface le reste du mois.", " - `state` n'est pas modifiable ici : la validation passe par le workflow BoondManager (`boond_validations_search` pour suivre).", "", "**6. Confirmer** : ID du CRA, nombre de jours saisis par imputation, et rappeler que le CRA reste à soumettre / valider dans BoondManager.");
|
|
917
|
+
return lines.filter(Boolean).join("\n");
|
|
918
|
+
},
|
|
919
|
+
},
|
|
518
920
|
];
|
|
921
|
+
// ---- Assisted entry of entity arguments (`completions/complete`, issue #260) --
|
|
922
|
+
//
|
|
923
|
+
// Every `*_id` argument accepts an id or a label (see `resolveEntity`), so the
|
|
924
|
+
// completer returns *labels* — "Prénom Nom", a company name — which the user
|
|
925
|
+
// can pick and which the runbook then resolves; `CompleteResult.values` is
|
|
926
|
+
// substituted verbatim, so ids would be opaque in the picker. Keyed by
|
|
927
|
+
// argument name: `manager_id` / `resource_id` → resources, `society_id` →
|
|
928
|
+
// companies, and so on. `completable()` attaches the completer to the same
|
|
929
|
+
// Zod object (a non-enumerable symbol), so the mirror workflow tools keep
|
|
930
|
+
// advertising the unchanged JSON Schema.
|
|
931
|
+
const COMPLETION_KIND_BY_ARG = {
|
|
932
|
+
manager_id: "resource",
|
|
933
|
+
resource_id: "resource",
|
|
934
|
+
society_id: "society",
|
|
935
|
+
opportunity_id: "opportunity",
|
|
936
|
+
agency_id: "agency",
|
|
937
|
+
project_id: "project",
|
|
938
|
+
candidate_id: "candidate",
|
|
939
|
+
contact_id: "contact",
|
|
940
|
+
// `ingest_communication` (#180) spells it the French way.
|
|
941
|
+
opportunite_id: "opportunity",
|
|
942
|
+
};
|
|
943
|
+
const SEARCH_PATH_BY_KIND = {
|
|
944
|
+
resource: "/resources",
|
|
945
|
+
society: "/companies",
|
|
946
|
+
opportunity: "/opportunities",
|
|
947
|
+
agency: "/agencies",
|
|
948
|
+
project: "/projects",
|
|
949
|
+
candidate: "/candidates",
|
|
950
|
+
contact: "/contacts",
|
|
951
|
+
};
|
|
952
|
+
const MAX_COMPLETIONS = 8;
|
|
953
|
+
function labelOf(attrs) {
|
|
954
|
+
const person = [attrs.firstName, attrs.lastName].filter((v) => typeof v === "string" && v.length > 0).join(" ");
|
|
955
|
+
if (person)
|
|
956
|
+
return person;
|
|
957
|
+
for (const key of ["name", "title", "reference"]) {
|
|
958
|
+
const v = attrs[key];
|
|
959
|
+
if (typeof v === "string" && v.trim().length > 0)
|
|
960
|
+
return v.trim();
|
|
961
|
+
}
|
|
962
|
+
return undefined;
|
|
963
|
+
}
|
|
964
|
+
/** Search the entity by the typed prefix; empty on any failure (no credentials, API down). */
|
|
965
|
+
export async function completeEntityArgument(kind, value) {
|
|
966
|
+
const needle = value.trim();
|
|
967
|
+
if (needle.length === 0)
|
|
968
|
+
return [];
|
|
969
|
+
try {
|
|
970
|
+
const response = await apiRequest(SEARCH_PATH_BY_KIND[kind], "GET", undefined, {
|
|
971
|
+
keywords: needle,
|
|
972
|
+
maxResults: MAX_COMPLETIONS,
|
|
973
|
+
});
|
|
974
|
+
const rows = Array.isArray(response.data) ? response.data : response.data ? [response.data] : [];
|
|
975
|
+
const labels = rows.map((r) => labelOf(r?.attributes ?? {})).filter((l) => !!l);
|
|
976
|
+
return [...new Set(labels)].slice(0, MAX_COMPLETIONS);
|
|
977
|
+
}
|
|
978
|
+
catch {
|
|
979
|
+
return [];
|
|
980
|
+
}
|
|
981
|
+
}
|
|
982
|
+
for (const p of PROMPTS) {
|
|
983
|
+
for (const [arg, schema] of Object.entries(p.argsSchema)) {
|
|
984
|
+
const kind = COMPLETION_KIND_BY_ARG[arg];
|
|
985
|
+
if (kind)
|
|
986
|
+
completable(schema, (value) => completeEntityArgument(kind, String(value ?? "")));
|
|
987
|
+
}
|
|
988
|
+
}
|
|
519
989
|
export function registerAllPrompts(server, policy) {
|
|
520
990
|
for (const p of PROMPTS) {
|
|
521
991
|
// Cut a prompt when any domain it orchestrates is filtered out, so the
|
|
@@ -526,7 +996,7 @@ export function registerAllPrompts(server, policy) {
|
|
|
526
996
|
title: p.title,
|
|
527
997
|
description: p.description,
|
|
528
998
|
argsSchema: p.argsSchema,
|
|
529
|
-
},
|
|
999
|
+
}, (args) => userMessage(p.build((args ?? {}))));
|
|
530
1000
|
}
|
|
531
1001
|
}
|
|
532
1002
|
/** Exposed for tests so we can assert names/coverage without instantiating a server. */
|