Files
Nexus/packages/api/src/router/assistant-tools.ts
T

7344 lines
269 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* AI Assistant Tool definitions for OpenAI Function Calling.
* Each tool has a JSON schema (for the AI) and an execute function (for the server).
*/
import { prisma, Prisma, ImportBatchStatus, StagedRecordStatus, DispoStagedRecordType, VacationType } from "@capakraken/db";
import {
CreateAssignmentSchema,
CreateClientSchema,
CreateCountrySchema,
type CreateEstimateInput,
CreateHolidayCalendarEntrySchema,
CreateHolidayCalendarSchema,
CreateProjectSchema,
CreateResourceSchema,
CreateMetroCitySchema,
CreateOrgUnitSchema,
CreateRoleSchema,
AllocationStatus,
EstimateExportFormat,
EstimateStatus,
PermissionKey,
PreviewResolvedHolidaysSchema,
SystemRole,
UpdateClientSchema,
type UpdateEstimateDraftInput,
UpdateCountrySchema,
UpdateHolidayCalendarEntrySchema,
UpdateHolidayCalendarSchema,
UpdateAssignmentSchema,
UpdateMetroCitySchema,
UpdateOrgUnitSchema,
UpdateProjectSchema,
UpdateRoleSchema,
UpdateResourceSchema,
} from "@capakraken/shared";
import type { WeekdayAvailability } from "@capakraken/shared";
import { TRPCError } from "@trpc/server";
import { fmtEur } from "../lib/format-utils.js";
import { timelineRouter } from "./timeline.js";
import { logger } from "../lib/logger.js";
import { createCallerFactory, type TRPCContext } from "../trpc.js";
import { auditLogRouter } from "./audit-log.js";
import { chargeabilityReportRouter } from "./chargeability-report.js";
import { computationGraphRouter } from "./computation-graph.js";
import { dispoRouter } from "./dispo.js";
import { importExportRouter } from "./import-export.js";
import { resourceRouter } from "./resource.js";
import { settingsRouter } from "./settings.js";
import { systemRoleConfigRouter } from "./system-role-config.js";
import { userRouter } from "./user.js";
import { notificationRouter } from "./notification.js";
import { estimateRouter } from "./estimate.js";
import { webhookRouter } from "./webhook.js";
import { countryRouter } from "./country.js";
import { holidayCalendarRouter } from "./holiday-calendar.js";
import { blueprintRouter } from "./blueprint.js";
import { roleRouter } from "./role.js";
import { clientRouter } from "./client.js";
import { orgUnitRouter } from "./org-unit.js";
import { projectRouter } from "./project.js";
import { rateCardRouter } from "./rate-card.js";
import { reportRouter } from "./report.js";
import { vacationRouter } from "./vacation.js";
import { entitlementRouter } from "./entitlement.js";
import { commentRouter } from "./comment.js";
import { managementLevelRouter } from "./management-level.js";
import { utilizationCategoryRouter } from "./utilization-category.js";
import { calculationRuleRouter } from "./calculation-rules.js";
import { effortRuleRouter } from "./effort-rule.js";
import { experienceMultiplierRouter } from "./experience-multiplier.js";
import { dashboardRouter } from "./dashboard.js";
import { insightsRouter } from "./insights.js";
import { scenarioRouter } from "./scenario.js";
import { allocationRouter } from "./allocation.js";
import { staffingRouter } from "./staffing.js";
// ─── Mutation tool set for audit logging (EGAI 4.1.3.1 / IAAI 3.6.26) ──────
export const MUTATION_TOOLS = new Set([
"import_csv_data",
"update_system_settings",
"test_ai_connection",
"test_smtp_connection",
"test_gemini_connection",
"update_system_role_config",
"create_webhook",
"update_webhook",
"delete_webhook",
"test_webhook",
"stage_dispo_import_batch",
"cancel_dispo_import_batch",
"resolve_dispo_staged_record",
"commit_dispo_import_batch",
"create_allocation", "cancel_allocation", "update_allocation_status",
"update_timeline_allocation_inline", "apply_timeline_project_shift",
"quick_assign_timeline_resource", "batch_quick_assign_timeline_resources",
"batch_shift_timeline_allocations",
"update_resource", "deactivate_resource", "create_resource",
"update_project", "create_project", "delete_project",
"create_vacation", "approve_vacation", "reject_vacation", "cancel_vacation",
"set_entitlement", "create_demand", "fill_demand",
"generate_project_cover", "remove_project_cover",
"create_role", "update_role", "delete_role",
"create_client", "update_client",
"create_org_unit", "update_org_unit",
"create_country", "update_country",
"create_metro_city", "update_metro_city", "delete_metro_city",
"create_holiday_calendar", "update_holiday_calendar", "delete_holiday_calendar",
"create_holiday_calendar_entry", "update_holiday_calendar_entry", "delete_holiday_calendar_entry",
"send_broadcast", "create_task_for_user", "create_reminder",
"update_task_status", "execute_task_action",
"create_comment", "resolve_comment", "mark_notification_read",
"save_dashboard_layout", "toggle_favorite_project",
"set_column_preferences", "generate_totp_secret", "verify_and_enable_totp",
"create_user", "set_user_password", "update_user_role", "update_user_name",
"link_user_resource", "auto_link_users_by_email", "set_user_permissions",
"reset_user_permissions", "disable_user_totp",
"create_notification", "update_reminder", "delete_reminder",
"delete_notification", "assign_task",
"clone_estimate", "update_estimate_draft", "submit_estimate_version",
"approve_estimate_version", "create_estimate_revision",
"create_estimate_export", "create_estimate_planning_handoff",
"generate_estimate_weekly_phasing", "update_estimate_commercial_terms",
]);
export const ADVANCED_ASSISTANT_TOOLS = new Set([
"find_best_project_resource",
"get_timeline_entries_view",
"get_timeline_holiday_overlays",
"get_project_timeline_context",
"preview_project_shift",
"update_timeline_allocation_inline",
"apply_timeline_project_shift",
"quick_assign_timeline_resource",
"batch_quick_assign_timeline_resources",
"batch_shift_timeline_allocations",
"get_chargeability_report",
"get_resource_computation_graph",
"get_project_computation_graph",
]);
// ─── Types ──────────────────────────────────────────────────────────────────
export type ToolContext = {
db: typeof prisma;
userId: string;
userRole: string;
permissions: Set<PermissionKey>;
session?: TRPCContext["session"];
dbUser?: TRPCContext["dbUser"];
roleDefaults?: TRPCContext["roleDefaults"];
};
export interface ToolDef {
type: "function";
function: {
name: string;
description: string;
parameters: Record<string, unknown>;
};
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
type ToolExecutor = (params: any, ctx: ToolContext) => Promise<unknown>;
const createChargeabilityReportCaller = createCallerFactory(chargeabilityReportRouter);
const createComputationGraphCaller = createCallerFactory(computationGraphRouter);
const createTimelineCaller = createCallerFactory(timelineRouter);
const createAuditLogCaller = createCallerFactory(auditLogRouter);
const createImportExportCaller = createCallerFactory(importExportRouter);
const createDispoCaller = createCallerFactory(dispoRouter);
const createResourceCaller = createCallerFactory(resourceRouter);
const createSettingsCaller = createCallerFactory(settingsRouter);
const createSystemRoleConfigCaller = createCallerFactory(systemRoleConfigRouter);
const createUserCaller = createCallerFactory(userRouter);
const createNotificationCaller = createCallerFactory(notificationRouter);
const createEstimateCaller = createCallerFactory(estimateRouter);
const createWebhookCaller = createCallerFactory(webhookRouter);
const createCountryCaller = createCallerFactory(countryRouter);
const createHolidayCalendarCaller = createCallerFactory(holidayCalendarRouter);
const createBlueprintCaller = createCallerFactory(blueprintRouter);
const createRoleCaller = createCallerFactory(roleRouter);
const createClientCaller = createCallerFactory(clientRouter);
const createOrgUnitCaller = createCallerFactory(orgUnitRouter);
const createProjectCaller = createCallerFactory(projectRouter);
const createRateCardCaller = createCallerFactory(rateCardRouter);
const createReportCaller = createCallerFactory(reportRouter);
const createVacationCaller = createCallerFactory(vacationRouter);
const createEntitlementCaller = createCallerFactory(entitlementRouter);
const createCommentCaller = createCallerFactory(commentRouter);
const createManagementLevelCaller = createCallerFactory(managementLevelRouter);
const createUtilizationCategoryCaller = createCallerFactory(utilizationCategoryRouter);
const createCalculationRuleCaller = createCallerFactory(calculationRuleRouter);
const createEffortRuleCaller = createCallerFactory(effortRuleRouter);
const createExperienceMultiplierCaller = createCallerFactory(experienceMultiplierRouter);
const createDashboardCaller = createCallerFactory(dashboardRouter);
const createInsightsCaller = createCallerFactory(insightsRouter);
const createScenarioCaller = createCallerFactory(scenarioRouter);
const createAllocationCaller = createCallerFactory(allocationRouter);
const createStaffingCaller = createCallerFactory(staffingRouter);
// ─── Helpers ────────────────────────────────────────────────────────────────
function fmtDate(d: Date | null | undefined): string | null {
return d ? d.toISOString().slice(0, 10) : null;
}
function assertPermission(ctx: ToolContext, perm: PermissionKey): void {
if (!ctx.permissions.has(perm)) {
throw new Error(`Permission denied: you need the "${perm}" permission to perform this action.`);
}
}
function assertAdminRole(ctx: ToolContext): void {
if (ctx.userRole !== SystemRole.ADMIN) {
throw new Error("Admin role required to perform this action.");
}
}
function formatHolidayCalendarEntry(entry: {
id: string;
date: Date;
name: string;
isRecurringAnnual?: boolean | null;
source?: string | null;
}) {
return {
id: entry.id,
date: fmtDate(entry.date),
name: entry.name,
isRecurringAnnual: entry.isRecurringAnnual ?? false,
source: entry.source ?? null,
};
}
function formatHolidayCalendar(calendar: {
id: string;
name: string;
scopeType: string;
stateCode?: string | null;
isActive?: boolean | null;
priority?: number | null;
country?: { id: string; code: string; name: string } | null;
metroCity?: { id: string; name: string } | null;
_count?: { entries?: number | null } | null;
entries?: Array<{
id: string;
date: Date;
name: string;
isRecurringAnnual?: boolean | null;
source?: string | null;
}> | null;
}) {
const entries = calendar.entries?.map(formatHolidayCalendarEntry) ?? [];
return {
id: calendar.id,
name: calendar.name,
scopeType: calendar.scopeType,
stateCode: calendar.stateCode ?? null,
isActive: calendar.isActive ?? true,
priority: calendar.priority ?? 0,
country: calendar.country
? {
id: calendar.country.id,
code: calendar.country.code,
name: calendar.country.name,
}
: null,
metroCity: calendar.metroCity
? {
id: calendar.metroCity.id,
name: calendar.metroCity.name,
}
: null,
entryCount: calendar._count?.entries ?? entries.length,
entries,
};
}
function formatCountry(country: {
id: string;
code: string;
name: string;
dailyWorkingHours: number;
scheduleRules?: Prisma.JsonValue | null;
isActive?: boolean | null;
metroCities?: Array<{ id: string; name: string }> | null;
_count?: { resources?: number | null } | null;
}) {
return {
id: country.id,
code: country.code,
name: country.name,
dailyWorkingHours: country.dailyWorkingHours,
scheduleRules: country.scheduleRules ?? null,
isActive: country.isActive ?? true,
resourceCount: country._count?.resources ?? null,
metroCities: (country.metroCities ?? []).map((city) => ({
id: city.id,
name: city.name,
})),
cities: (country.metroCities ?? []).map((city) => city.name),
};
}
function createUtcDate(year: number, monthIndex: number, day: number): Date {
return new Date(Date.UTC(year, monthIndex, day));
}
function resolveHolidayPeriod(input: {
year?: number;
periodStart?: string;
periodEnd?: string;
}): { year: number | null; periodStart: Date; periodEnd: Date } {
if (input.periodStart || input.periodEnd) {
if (!input.periodStart || !input.periodEnd) {
throw new Error("periodStart and periodEnd must both be provided when using a custom holiday range.");
}
const periodStart = new Date(`${input.periodStart}T00:00:00.000Z`);
const periodEnd = new Date(`${input.periodEnd}T00:00:00.000Z`);
if (Number.isNaN(periodStart.getTime())) {
throw new Error(`Invalid periodStart: ${input.periodStart}`);
}
if (Number.isNaN(periodEnd.getTime())) {
throw new Error(`Invalid periodEnd: ${input.periodEnd}`);
}
if (periodEnd < periodStart) {
throw new Error("periodEnd must be on or after periodStart.");
}
return { year: null, periodStart, periodEnd };
}
const year = input.year ?? new Date().getUTCFullYear();
return {
year,
periodStart: createUtcDate(year, 0, 1),
periodEnd: createUtcDate(year, 11, 31),
};
}
const ASSISTANT_VACATION_REQUEST_TYPES = [
VacationType.ANNUAL,
VacationType.SICK,
VacationType.OTHER,
] as const;
function parseAssistantVacationRequestType(input: string): VacationType {
const normalized = input.trim().toUpperCase();
if (normalized === VacationType.PUBLIC_HOLIDAY) {
throw new Error("PUBLIC_HOLIDAY requests cannot be created manually. Manage public holidays through holiday calendars instead.");
}
if ((ASSISTANT_VACATION_REQUEST_TYPES as readonly string[]).includes(normalized)) {
return normalized as VacationType;
}
throw new Error(`Invalid vacation type: ${input}. Valid types: ${ASSISTANT_VACATION_REQUEST_TYPES.join(", ")}.`);
}
function parseIsoDate(value: string, fieldName: string): Date {
const parsed = new Date(`${value}T00:00:00.000Z`);
if (Number.isNaN(parsed.getTime())) {
throw new Error(`Invalid ${fieldName}: ${value}`);
}
return parsed;
}
function toDate(value: Date | string): Date {
return value instanceof Date ? value : new Date(value);
}
type AssistantToolErrorResult = { error: string };
function toAssistantNotFoundError(
error: unknown,
message: string,
): AssistantToolErrorResult | null {
if (error instanceof TRPCError && error.code === "NOT_FOUND") {
return { error: message };
}
return null;
}
async function resolveEntityOrAssistantError<T>(
resolve: () => Promise<T>,
notFoundMessage: string,
): Promise<T | AssistantToolErrorResult> {
try {
return await resolve();
} catch (error) {
const mapped = toAssistantNotFoundError(error, notFoundMessage);
if (mapped) {
return mapped;
}
throw error;
}
}
async function resolveProjectIdentifier(
ctx: ToolContext,
identifier: string,
) {
const caller = createProjectCaller(createScopedCallerContext(ctx));
return resolveEntityOrAssistantError(
() => caller.resolveByIdentifier({ identifier }),
`Project not found: ${identifier}`,
);
}
async function resolveResourceIdentifier(
ctx: ToolContext,
identifier: string,
) {
const caller = createResourceCaller(createScopedCallerContext(ctx));
return resolveEntityOrAssistantError(
() => caller.resolveByIdentifier({ identifier }),
`Resource not found: ${identifier}`,
);
}
function createScopedCallerContext(ctx: ToolContext): TRPCContext {
if (!ctx.session?.user || !ctx.dbUser) {
throw new Error("Authenticated assistant context is required for this tool.");
}
return {
session: ctx.session,
db: ctx.db,
dbUser: ctx.dbUser,
roleDefaults: ctx.roleDefaults ?? null,
};
}
function sanitizeWebhook<T extends { secret?: string | null }>(webhook: T) {
const { secret: _secret, ...rest } = webhook;
return {
...rest,
hasSecret: Boolean(webhook.secret),
};
}
function sanitizeWebhookList<T extends { secret?: string | null }>(webhooks: T[]) {
return webhooks.map((webhook) => sanitizeWebhook(webhook));
}
// ─── Tool Definitions ───────────────────────────────────────────────────────
export const TOOL_DEFINITIONS: ToolDef[] = [
// ── READ TOOLS ──
{
type: "function",
function: {
name: "search_resources",
description: "Search for resources (employees) by name, employee ID, chapter, country, metro city, org unit, or role. Returns a list of matching resources with key details.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search term (matches displayName, eid, chapter)" },
country: { type: "string", description: "Filter by country name or code (e.g. 'Spain', 'ES', 'Deutschland', 'DE')" },
metroCity: { type: "string", description: "Filter by metro city name (e.g. 'Madrid', 'München')" },
orgUnit: { type: "string", description: "Filter by org unit name (partial match)" },
roleName: { type: "string", description: "Filter by role name (partial match)" },
isActive: { type: "boolean", description: "Filter by active status. Default: true" },
limit: { type: "integer", description: "Max results. Default: 50" },
},
},
},
},
{
type: "function",
function: {
name: "get_resource",
description: "Get detailed information about a single resource by ID, employee ID (eid), or name.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Resource ID, employee ID (eid like EMP-001), or display name" },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "search_projects",
description: "Search for projects by name, short code, status, or client.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search term (matches name, shortCode)" },
status: { type: "string", description: "Filter by status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_project",
description: "Get detailed information about a single project by ID or short code, including top allocations.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Project ID or short code (e.g. Z033T593)" },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "find_best_project_resource",
description: "Advanced assistant tool: find the best already-assigned resource on a project for a given period, ranked by remaining capacity or LCR. Holiday- and vacation-aware. Requires viewCosts and advanced assistant permissions.",
parameters: {
type: "object",
properties: {
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
minHoursPerDay: { type: "number", description: "Minimum remaining availability per effective working day. Default: 3." },
rankingMode: { type: "string", description: "Ranking mode: lowest_lcr, highest_remaining_hours_per_day, or highest_remaining_hours. Default: lowest_lcr." },
chapter: { type: "string", description: "Optional chapter filter for candidate resources." },
roleName: { type: "string", description: "Optional role filter for candidate resources." },
},
required: ["projectIdentifier"],
},
},
},
{
type: "function",
function: {
name: "get_timeline_entries_view",
description: "Advanced assistant tool: read-only timeline entries view with the same timeline/disposition readmodel used by the app. Returns allocations, demands, assignments, and matching holiday overlays for a period.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
resourceIds: { type: "array", items: { type: "string" }, description: "Optional resource IDs to scope the view." },
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to scope the view." },
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to scope the view." },
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters." },
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs to scope the view." },
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes such as DE or ES." },
},
},
},
},
{
type: "function",
function: {
name: "get_timeline_holiday_overlays",
description: "Advanced assistant tool: read-only holiday overlays for the timeline, resolved with the same holiday logic as the app. Useful to explain regional holiday differences for assigned or filtered resources.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD. Default: today." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD. If omitted, durationDays is used." },
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted. Default: 21." },
resourceIds: { type: "array", items: { type: "string" }, description: "Optional resource IDs to scope the overlays." },
projectIds: { type: "array", items: { type: "string" }, description: "Optional project IDs to scope the overlays via matching assignments." },
clientIds: { type: "array", items: { type: "string" }, description: "Optional client IDs to scope the overlays via matching projects." },
chapters: { type: "array", items: { type: "string" }, description: "Optional chapter filters." },
eids: { type: "array", items: { type: "string" }, description: "Optional employee IDs to scope the overlays." },
countryCodes: { type: "array", items: { type: "string" }, description: "Optional country codes such as DE or ES." },
},
},
},
},
{
type: "function",
function: {
name: "get_project_timeline_context",
description: "Advanced assistant tool: read-only project timeline/disposition context. Reuses the same project context readmodel as the app and adds holiday overlays plus cross-project overlap summaries for assigned resources.",
parameters: {
type: "object",
properties: {
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
startDate: { type: "string", description: "Optional holiday/conflict window start date in YYYY-MM-DD. Defaults to the project start date when available." },
endDate: { type: "string", description: "Optional holiday/conflict window end date in YYYY-MM-DD. Defaults to the project end date when available." },
durationDays: { type: "integer", description: "Optional duration in calendar days when endDate is omitted." },
},
required: ["projectIdentifier"],
},
},
},
{
type: "function",
function: {
name: "preview_project_shift",
description: "Advanced assistant tool: read-only preview of the timeline shift validation for a project. Uses the same preview logic as the timeline router and does not write changes.",
parameters: {
type: "object",
properties: {
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
newStartDate: { type: "string", description: "New start date in YYYY-MM-DD." },
newEndDate: { type: "string", description: "New end date in YYYY-MM-DD." },
},
required: ["projectIdentifier", "newStartDate", "newEndDate"],
},
},
},
{
type: "function",
function: {
name: "update_timeline_allocation_inline",
description: "Advanced assistant mutation: update a timeline allocation inline with the same manager/admin + manageAllocations validation as the timeline API. Supports hours/day, dates, includeSaturday, and role changes. Requires useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
allocationId: { type: "string", description: "Allocation, assignment, or demand row ID to update." },
hoursPerDay: { type: "number", description: "Optional new booked hours per day." },
startDate: { type: "string", description: "Optional new start date in YYYY-MM-DD." },
endDate: { type: "string", description: "Optional new end date in YYYY-MM-DD." },
includeSaturday: { type: "boolean", description: "Optional Saturday-working flag stored in metadata." },
role: { type: "string", description: "Optional new role label." },
},
required: ["allocationId"],
},
},
},
{
type: "function",
function: {
name: "apply_timeline_project_shift",
description: "Advanced assistant mutation: apply the real timeline project shift mutation, including validation, date movement, cost recalculation, audit logging, and SSE. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
newStartDate: { type: "string", description: "New project start date in YYYY-MM-DD." },
newEndDate: { type: "string", description: "New project end date in YYYY-MM-DD." },
},
required: ["projectIdentifier", "newStartDate", "newEndDate"],
},
},
},
{
type: "function",
function: {
name: "quick_assign_timeline_resource",
description: "Advanced assistant mutation: create a timeline quick-assignment with the same manager/admin + manageAllocations rules as the timeline UI. Resolves resource and project identifiers before calling the real mutation. Requires useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
resourceIdentifier: { type: "string", description: "Resource ID, eid, or display name." },
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
role: { type: "string", description: "Role label. Default: Team Member." },
roleId: { type: "string", description: "Optional concrete role ID." },
status: { type: "string", enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"], description: "Assignment status. Default: PROPOSED." },
},
required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "batch_quick_assign_timeline_resources",
description: "Advanced assistant mutation: batch-create timeline quick-assignments using the same timeline router logic, permission checks, and audit/SSE side effects as the app. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
assignments: {
type: "array",
minItems: 1,
maxItems: 50,
items: {
type: "object",
properties: {
resourceIdentifier: { type: "string", description: "Resource ID, eid, or display name." },
projectIdentifier: { type: "string", description: "Project ID, short code, or project name." },
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
hoursPerDay: { type: "number", description: "Hours per day. Default: 8." },
role: { type: "string", description: "Role label. Default: Team Member." },
status: { type: "string", enum: ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"], description: "Assignment status. Default: PROPOSED." },
},
required: ["resourceIdentifier", "projectIdentifier", "startDate", "endDate"],
},
description: "Assignment rows to create in one batch.",
},
},
required: ["assignments"],
},
},
},
{
type: "function",
function: {
name: "batch_shift_timeline_allocations",
description: "Advanced assistant mutation: shift multiple timeline allocations by a shared day delta using the real timeline batch move/resize mutation. Requires manager/admin, manageAllocations, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
allocationIds: { type: "array", items: { type: "string" }, description: "Allocation IDs to shift." },
daysDelta: { type: "integer", description: "Signed day delta to apply." },
mode: { type: "string", enum: ["move", "resize-start", "resize-end"], description: "Shift mode. Default: move." },
},
required: ["allocationIds", "daysDelta"],
},
},
},
{
type: "function",
function: {
name: "list_allocations",
description: "List assignments/allocations, optionally filtered by resource or project. Shows who is assigned where, hours/day, dates, and cost.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Filter by resource ID" },
projectId: { type: "string", description: "Filter by project ID" },
resourceName: { type: "string", description: "Filter by resource name (partial match)" },
projectCode: { type: "string", description: "Filter by project short code (partial match)" },
status: { type: "string", description: "Filter by status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
limit: { type: "integer", description: "Max results. Default: 30" },
},
},
},
},
{
type: "function",
function: {
name: "get_budget_status",
description: "Get the budget status of a project: total budget, confirmed/proposed costs, remaining, utilization percentage.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "get_vacation_balance",
description: "Get the holiday-aware vacation balance for a resource via the real entitlement workflow. Authenticated users can read their own balance; manager/admin/controller can read broader balances.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
year: { type: "integer", description: "Year. Default: current year" },
},
required: ["resourceId"],
},
},
},
{
type: "function",
function: {
name: "list_vacations_upcoming",
description: "List upcoming vacations across all resources, or for a specific resource/team. Shows who is off when.",
parameters: {
type: "object",
properties: {
resourceName: { type: "string", description: "Filter by resource name (partial match)" },
chapter: { type: "string", description: "Filter by chapter/team" },
daysAhead: { type: "integer", description: "How many days ahead to look. Default: 30" },
limit: { type: "integer", description: "Max results. Default: 30" },
},
},
},
},
{
type: "function",
function: {
name: "list_holidays_by_region",
description: "List resolved public holidays for a country, federal state, and optionally a city in a given year or date range. Use this to compare regions such as Bayern vs Hamburg.",
parameters: {
type: "object",
properties: {
countryCode: { type: "string", description: "Country code such as DE, ES, US, IN." },
federalState: { type: "string", description: "Federal state / region code, e.g. BY, HH, NRW." },
metroCity: { type: "string", description: "Optional city name for local city-specific holidays, e.g. Augsburg." },
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
periodStart: { type: "string", description: "Optional start date in YYYY-MM-DD. Requires periodEnd." },
periodEnd: { type: "string", description: "Optional end date in YYYY-MM-DD. Requires periodStart." },
},
required: ["countryCode"],
},
},
},
{
type: "function",
function: {
name: "get_resource_holidays",
description: "List resolved public holidays for a specific resource based on that person's country, federal state, and city context.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Resource ID, EID, or display name." },
year: { type: "integer", description: "Full year, e.g. 2026. Default: current year." },
periodStart: { type: "string", description: "Optional start date in YYYY-MM-DD. Requires periodEnd." },
periodEnd: { type: "string", description: "Optional end date in YYYY-MM-DD. Requires periodStart." },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "list_holiday_calendars",
description: "List holiday calendars including scope, assignment, active flag, priority, and entry count. Useful to inspect the calendar-editor configuration context.",
parameters: {
type: "object",
properties: {
includeInactive: { type: "boolean", description: "Include inactive calendars. Default: false." },
countryCode: { type: "string", description: "Optional country code filter such as DE or ES." },
scopeType: { type: "string", description: "Optional scope filter: COUNTRY, STATE, CITY." },
stateCode: { type: "string", description: "Optional state/region code filter such as BY or NRW." },
metroCity: { type: "string", description: "Optional city-name filter." },
},
},
},
},
{
type: "function",
function: {
name: "get_holiday_calendar",
description: "Get a single holiday calendar including all entries. Accepts either the calendar ID or its name.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Holiday calendar ID or name." },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "preview_resolved_holiday_calendar",
description: "Preview the resolved holiday result for a country/state/city scope and year, including which calendar each holiday comes from.",
parameters: {
type: "object",
properties: {
countryId: { type: "string", description: "Country ID." },
stateCode: { type: "string", description: "Optional state/region code." },
metroCityId: { type: "string", description: "Optional metro city ID for city-specific preview." },
year: { type: "integer", description: "Full year, e.g. 2026." },
},
required: ["countryId", "year"],
},
},
},
{
type: "function",
function: {
name: "create_holiday_calendar",
description: "Create a holiday calendar for a country, state, or city scope. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Calendar name." },
scopeType: { type: "string", description: "COUNTRY, STATE, or CITY." },
countryId: { type: "string", description: "Country ID." },
stateCode: { type: "string", description: "Required for STATE calendars." },
metroCityId: { type: "string", description: "Required for CITY calendars." },
isActive: { type: "boolean", description: "Whether the calendar is active. Default: true." },
priority: { type: "integer", description: "Priority used during calendar resolution. Default: 0." },
},
required: ["name", "scopeType", "countryId"],
},
},
},
{
type: "function",
function: {
name: "update_holiday_calendar",
description: "Update an existing holiday calendar. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar ID." },
data: {
type: "object",
properties: {
name: { type: "string" },
stateCode: { type: "string" },
metroCityId: { type: "string" },
isActive: { type: "boolean" },
priority: { type: "integer" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_holiday_calendar",
description: "Delete a holiday calendar and all of its entries. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "create_holiday_calendar_entry",
description: "Create a holiday entry in an existing calendar. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
holidayCalendarId: { type: "string", description: "Holiday calendar ID." },
date: { type: "string", description: "Date in YYYY-MM-DD format." },
name: { type: "string", description: "Holiday name." },
isRecurringAnnual: { type: "boolean", description: "Whether the holiday repeats every year." },
source: { type: "string", description: "Optional source or legal basis." },
},
required: ["holidayCalendarId", "date", "name"],
},
},
},
{
type: "function",
function: {
name: "update_holiday_calendar_entry",
description: "Update an existing holiday calendar entry. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar entry ID." },
data: {
type: "object",
properties: {
date: { type: "string", description: "Date in YYYY-MM-DD format." },
name: { type: "string" },
isRecurringAnnual: { type: "boolean" },
source: { type: "string" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_holiday_calendar_entry",
description: "Delete a holiday calendar entry. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Holiday calendar entry ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "list_roles",
description: "List all available roles with their colors.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "search_by_skill",
description: "Find resources that have a specific skill.",
parameters: {
type: "object",
properties: {
skill: { type: "string", description: "Skill name to search for" },
},
required: ["skill"],
},
},
},
{
type: "function",
function: {
name: "get_statistics",
description: "Get overview statistics: total resources, projects, active allocations, budget summary, projects by status, chapter breakdown.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_chargeability",
description: "Get chargeability data for a resource in a given month: hours booked vs available, chargeability %, target comparison.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, eid, or name" },
month: { type: "string", description: "Month in YYYY-MM format, e.g. 2026-03. Default: current month" },
},
required: ["resourceId"],
},
},
},
{
type: "function",
function: {
name: "get_chargeability_report",
description: "Get the detailed chargeability report readmodel for a month range, including group totals and per-resource month series. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
startMonth: { type: "string", description: "Start month in YYYY-MM format." },
endMonth: { type: "string", description: "End month in YYYY-MM format." },
orgUnitId: { type: "string", description: "Optional org unit filter." },
managementLevelGroupId: { type: "string", description: "Optional management level group filter." },
countryId: { type: "string", description: "Optional country filter." },
includeProposed: { type: "boolean", description: "Whether proposed bookings should count towards chargeability. Default: false." },
resourceQuery: { type: "string", description: "Optional resource filter by name or eid after loading the report." },
resourceLimit: { type: "integer", description: "Maximum number of resources returned. Default: 25, max 100." },
},
required: ["startMonth", "endMonth"],
},
},
},
{
type: "function",
function: {
name: "get_resource_computation_graph",
description: "Get the resource computation graph with transparent SAH, holiday, absence, allocation, chargeability, and budget derivation factors. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, eid, or display name." },
month: { type: "string", description: "Month in YYYY-MM format." },
domain: { type: "string", enum: ["INPUT", "SAH", "ALLOCATION", "RULES", "CHARGEABILITY", "BUDGET"], description: "Optional domain filter for graph nodes." },
includeLinks: { type: "boolean", description: "Include graph links for the selected nodes. Default: false." },
},
required: ["resourceId", "month"],
},
},
},
{
type: "function",
function: {
name: "get_project_computation_graph",
description: "Get the project computation graph with estimate, commercial, effort, experience, spread, and budget derivation factors. Requires controller/manager/admin access, viewCosts, and useAssistantAdvancedTools.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID, short code, or project name." },
domain: { type: "string", enum: ["INPUT", "ESTIMATE", "COMMERCIAL", "EXPERIENCE", "EFFORT", "SPREAD", "BUDGET"], description: "Optional domain filter for graph nodes." },
includeLinks: { type: "boolean", description: "Include graph links for the selected nodes. Default: false." },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "search_estimates",
description: "Search for estimates (cost/effort estimates) by project or name. Returns estimate name, status, version count.",
parameters: {
type: "object",
properties: {
projectCode: { type: "string", description: "Project short code to filter by" },
query: { type: "string", description: "Search term (matches estimate name)" },
status: { type: "string", description: "Filter by status: DRAFT, IN_REVIEW, APPROVED, ARCHIVED" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "list_clients",
description: "List clients/customers. Can search by name or code.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search term (matches name or code)" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "list_org_units",
description: "List organizational units (departments, teams) with their hierarchy.",
parameters: {
type: "object",
properties: {
level: { type: "integer", description: "Filter by org level (5, 6, or 7)" },
},
},
},
},
// ── NAVIGATION TOOLS ──
{
type: "function",
function: {
name: "navigate_to_page",
description: "Navigate the user to a specific page in CapaKraken, optionally with filters. Use this when the user wants to see data on a specific page (e.g. 'show me on the timeline', 'open the resources page').",
parameters: {
type: "object",
properties: {
page: {
type: "string",
description: "Page name: timeline, dashboard, resources, projects, allocations, staffing, estimates, vacations, my-vacations, roles, skills-analytics, chargeability, computation-graph",
},
eids: { type: "string", description: "Comma-separated employee IDs to filter (for timeline)" },
chapters: { type: "string", description: "Comma-separated chapters to filter (for timeline)" },
projectIds: { type: "string", description: "Comma-separated project IDs to filter (for timeline)" },
clientIds: { type: "string", description: "Comma-separated client IDs to filter (for timeline)" },
countryCodes: { type: "string", description: "Comma-separated country codes to filter (e.g. 'ES,DE' for Spain and Germany, for timeline)" },
startDate: { type: "string", description: "Start date YYYY-MM-DD (for timeline)" },
days: { type: "integer", description: "Number of days to show (for timeline)" },
},
required: ["page"],
},
},
},
// ── WRITE TOOLS ──
{
type: "function",
function: {
name: "create_allocation",
description: "Create a new allocation/booking for a resource on a project. Requires manageAllocations permission. Always confirm with the user before calling this. Created with PROPOSED status.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID" },
projectId: { type: "string", description: "Project ID" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
hoursPerDay: { type: "number", description: "Hours per day (e.g. 8)" },
role: { type: "string", description: "Optional role name" },
},
required: ["resourceId", "projectId", "startDate", "endDate", "hoursPerDay"],
},
},
},
{
type: "function",
function: {
name: "cancel_allocation",
description: "Cancel an existing allocation. Can find by allocation ID, or by resource name + project code + date range. Requires manageAllocations permission. Always confirm with the user before calling this.",
parameters: {
type: "object",
properties: {
allocationId: { type: "string", description: "Allocation ID (if known)" },
resourceName: { type: "string", description: "Resource name (partial match)" },
projectCode: { type: "string", description: "Project short code (partial match)" },
startDate: { type: "string", description: "Filter by start date YYYY-MM-DD" },
endDate: { type: "string", description: "Filter by end date YYYY-MM-DD" },
},
},
},
},
{
type: "function",
function: {
name: "update_resource",
description: "Update a resource's details. Requires manageResources permission. Always confirm with the user before calling this.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Resource ID, EID, or display name" },
displayName: { type: "string", description: "New display name" },
fte: { type: "number", description: "New FTE (0.0-1.0)" },
lcrCents: { type: "integer", description: "New LCR in cents (e.g. 8500 = 85.00 EUR/h)" },
chapter: { type: "string", description: "New chapter" },
chargeabilityTarget: { type: "number", description: "New chargeability target (0-100)" },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "update_allocation_status",
description: "Change the status of an existing allocation. Can reactivate cancelled allocations, confirm proposed ones, etc. Requires manageAllocations permission. Always confirm with the user before calling.",
parameters: {
type: "object",
properties: {
allocationId: { type: "string", description: "Allocation ID" },
resourceName: { type: "string", description: "Resource name (partial match, used if no allocationId)" },
projectCode: { type: "string", description: "Project short code (partial match, used if no allocationId)" },
startDate: { type: "string", description: "Start date filter YYYY-MM-DD (used if no allocationId)" },
newStatus: { type: "string", description: "New status: PROPOSED, CONFIRMED, ACTIVE, COMPLETED, CANCELLED" },
},
required: ["newStatus"],
},
},
},
{
type: "function",
function: {
name: "update_project",
description: "Update a project's details. Requires manageProjects permission. Always confirm with the user before calling this.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Project ID, short code, or project name" },
name: { type: "string", description: "New project name" },
budgetCents: { type: "integer", description: "New budget in cents (e.g. 10000000 = 100,000 EUR)" },
winProbability: { type: "integer", description: "Win probability 0-100" },
status: { type: "string", description: "New status: DRAFT, ACTIVE, ON_HOLD, COMPLETED, CANCELLED" },
responsiblePerson: { type: "string", description: "Name of the responsible person. Must match an existing resource's display name (case-insensitive search)." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "create_project",
description: "Create a new project. Requires manageProjects permission. Always confirm with the user before calling this. The project is created in DRAFT status by default.",
parameters: {
type: "object",
properties: {
shortCode: { type: "string", description: "Unique project code, uppercase alphanumeric with hyphens/underscores (e.g. 'PROJ-001')" },
name: { type: "string", description: "Project name" },
orderType: { type: "string", description: "Order type: BD, CHARGEABLE, INTERNAL, OVERHEAD" },
allocationType: { type: "string", description: "Allocation type: INT or EXT. Default: INT" },
budgetCents: { type: "integer", description: "Budget in cents (e.g. 10000000 = 100,000 EUR)" },
startDate: { type: "string", description: "Start date (YYYY-MM-DD)" },
endDate: { type: "string", description: "End date (YYYY-MM-DD)" },
winProbability: { type: "integer", description: "Win probability 0-100. Default: 100" },
status: { type: "string", description: "Initial status: DRAFT, ACTIVE, ON_HOLD. Default: DRAFT" },
responsiblePerson: { type: "string", description: "Name of the responsible person. Must match an existing resource's display name (case-insensitive search)." },
color: { type: "string", description: "Hex color (e.g. '#3b82f6')" },
blueprintName: { type: "string", description: "Blueprint name to look up and attach (partial match)" },
clientName: { type: "string", description: "Client name to look up and attach (partial match)" },
},
required: ["shortCode", "name", "orderType", "budgetCents", "startDate", "endDate", "responsiblePerson"],
},
},
},
// ── RESOURCE MANAGEMENT ──
{
type: "function",
function: {
name: "create_resource",
description: "Create a new resource (employee). Requires manageResources permission. Always confirm with the user before calling.",
parameters: {
type: "object",
properties: {
eid: { type: "string", description: "Employee ID (e.g. EMP-042)" },
displayName: { type: "string", description: "Full name" },
email: { type: "string", description: "Email address" },
fte: { type: "number", description: "FTE 0.0-1.0. Default: 1" },
lcrCents: { type: "integer", description: "Loaded cost rate in cents (e.g. 8500 = 85 EUR/h)" },
ucrCents: { type: "integer", description: "Unloaded cost rate in cents" },
chapter: { type: "string", description: "Chapter/team name" },
chargeabilityTarget: { type: "number", description: "Chargeability target 0-100. Default: 80" },
roleName: { type: "string", description: "Role name (partial match)" },
countryCode: { type: "string", description: "Country code (e.g. DE, ES)" },
orgUnitName: { type: "string", description: "Org unit name (partial match)" },
postalCode: { type: "string", description: "Postal code" },
},
required: ["eid", "displayName", "email", "lcrCents"],
},
},
},
{
type: "function",
function: {
name: "deactivate_resource",
description: "Deactivate a resource (soft delete). Requires manageResources permission. Always confirm first.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Resource ID, eid, or name" },
},
required: ["identifier"],
},
},
},
// ── VACATION MANAGEMENT ──
{
type: "function",
function: {
name: "create_vacation",
description: "Create a vacation/leave request through the real vacation workflow. Any authenticated user can request leave for their own resource; manager/admin can create requests for others. Always confirm with the user.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
type: {
type: "string",
enum: ["ANNUAL", "SICK", "OTHER"],
description: "Vacation type. PUBLIC_HOLIDAY requests are managed through holiday calendars, not manual vacation requests.",
},
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
isHalfDay: { type: "boolean", description: "Half day? Default: false" },
halfDayPart: { type: "string", description: "MORNING or AFTERNOON (if half day)" },
note: { type: "string", description: "Optional note" },
},
required: ["resourceId", "type", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "approve_vacation",
description: "Approve a vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "reject_vacation",
description: "Reject a pending vacation request through the real vacation workflow. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
reason: { type: "string", description: "Rejection reason" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "cancel_vacation",
description: "Cancel a vacation request through the real vacation workflow. Users can cancel their own requests; manager/admin can cancel any request. Always confirm first.",
parameters: {
type: "object",
properties: {
vacationId: { type: "string", description: "Vacation ID" },
},
required: ["vacationId"],
},
},
},
{
type: "function",
function: {
name: "get_pending_vacation_approvals",
description: "List vacation requests awaiting approval.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_team_vacation_overlap",
description: "Check if team members have overlapping vacations in a date range.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID to check overlap for" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
},
required: ["resourceId", "startDate", "endDate"],
},
},
},
// ── ENTITLEMENT ──
{
type: "function",
function: {
name: "get_entitlement_summary",
description: "Get vacation entitlement year summary for all resources or a specific resource.",
parameters: {
type: "object",
properties: {
year: { type: "integer", description: "Year. Default: current year" },
resourceName: { type: "string", description: "Filter by resource name (optional)" },
},
},
},
},
{
type: "function",
function: {
name: "set_entitlement",
description: "Set the annual vacation entitlement for a resource/year through the real entitlement workflow. Manager or admin role required. Carryover is computed automatically. Always confirm first.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, EID, or display name" },
year: { type: "integer", description: "Year" },
entitledDays: { type: "number", description: "Number of entitled vacation days" },
},
required: ["resourceId", "year", "entitledDays"],
},
},
},
// ── DEMAND / STAFFING ──
{
type: "function",
function: {
name: "list_demands",
description: "List staffing demand requirements for projects. Shows open positions that need to be filled.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Filter by project ID or short code" },
status: { type: "string", description: "Filter by status: OPEN, PARTIALLY_FILLED, FILLED, CANCELLED" },
limit: { type: "integer", description: "Max results. Default: 30" },
},
},
},
},
{
type: "function",
function: {
name: "create_demand",
description: "Create a staffing demand requirement on a project. Requires manageAllocations permission. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
roleName: { type: "string", description: "Role name for the demand" },
headcount: { type: "integer", description: "Number of people needed. Default: 1" },
hoursPerDay: { type: "number", description: "Hours per day required" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
},
required: ["projectId", "roleName", "hoursPerDay", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "fill_demand",
description: "Fill/assign a resource to an open demand requirement. Requires manageAllocations permission. Always confirm first.",
parameters: {
type: "object",
properties: {
demandId: { type: "string", description: "Demand requirement ID" },
resourceId: { type: "string", description: "Resource ID or name to assign" },
},
required: ["demandId", "resourceId"],
},
},
},
{
type: "function",
function: {
name: "check_resource_availability",
description: "Check if a resource is available in a given date range (no conflicting allocations or vacations).",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID, eid, or name" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
},
required: ["resourceId", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "get_staffing_suggestions",
description: "Get AI-powered staffing suggestions for a project based on required skills, availability, and cost.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
roleName: { type: "string", description: "Role to find candidates for" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
limit: { type: "integer", description: "Max suggestions. Default: 5" },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "find_capacity",
description: "Find resources with available capacity in a date range.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
minHoursPerDay: { type: "number", description: "Minimum available hours/day. Default: 4" },
roleName: { type: "string", description: "Filter by role name" },
chapter: { type: "string", description: "Filter by chapter" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
required: ["startDate", "endDate"],
},
},
},
// ── BLUEPRINT ──
{
type: "function",
function: {
name: "list_blueprints",
description: "List available project blueprints with their field definitions.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_blueprint",
description: "Get detailed blueprint with all field definitions and role presets.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Blueprint ID or name (partial match)" },
},
required: ["identifier"],
},
},
},
// ── RATE CARDS ──
{
type: "function",
function: {
name: "list_rate_cards",
description: "List rate cards with their effective dates and line items.",
parameters: {
type: "object",
properties: {
query: { type: "string", description: "Search by name" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "resolve_rate",
description: "Look up the applicable rate for a resource, role, or management level from rate cards.",
parameters: {
type: "object",
properties: {
resourceId: { type: "string", description: "Resource ID or name" },
roleName: { type: "string", description: "Role name" },
date: { type: "string", description: "Date to check rate for (YYYY-MM-DD). Default: today" },
},
},
},
},
// ── ESTIMATES ──
{
type: "function",
function: {
name: "get_estimate_detail",
description: "Get one estimate via the real estimate router, including versions, demand lines, metrics, assumptions, and linked project data. Controller/manager/admin access and viewCosts required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID" },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "list_estimate_versions",
description: "List estimate versions via the real estimate router, including status, timestamps, and artifact counts. Controller/manager/admin access required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID" },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "get_estimate_version_snapshot",
description: "Get an estimate version snapshot via the real estimate router, including totals, breakdowns, exports, and resource snapshots. Controller/manager/admin access and viewCosts required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID" },
versionId: { type: "string", description: "Optional explicit version ID. Defaults to the latest version." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "create_estimate",
description: "Create a new estimate via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Optional project ID." },
projectCode: { type: "string", description: "Optional project short code convenience alias." },
name: { type: "string", description: "Estimate name." },
opportunityId: { type: "string", description: "Optional opportunity/reference ID." },
baseCurrency: { type: "string", description: "Base currency, e.g. EUR." },
status: { type: "string", enum: ["DRAFT", "IN_REVIEW", "APPROVED", "ARCHIVED"] },
versionLabel: { type: "string", description: "Optional working version label." },
versionNotes: { type: "string", description: "Optional working version notes." },
assumptions: { type: "array", items: { type: "object" }, description: "Estimate assumptions." },
scopeItems: { type: "array", items: { type: "object" }, description: "Estimate scope items." },
demandLines: { type: "array", items: { type: "object" }, description: "Estimate demand lines." },
resourceSnapshots: { type: "array", items: { type: "object" }, description: "Resource cost snapshots." },
metrics: { type: "array", items: { type: "object" }, description: "Optional metric overrides." },
},
required: ["name"],
},
},
},
{
type: "function",
function: {
name: "clone_estimate",
description: "Clone an existing estimate via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
sourceEstimateId: { type: "string", description: "Source estimate ID." },
name: { type: "string", description: "Optional cloned estimate name." },
projectId: { type: "string", description: "Optional target project ID." },
projectCode: { type: "string", description: "Optional target project short code convenience alias." },
},
required: ["sourceEstimateId"],
},
},
},
{
type: "function",
function: {
name: "update_estimate_draft",
description: "Update the working draft of an estimate via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Estimate ID." },
projectId: { type: "string", description: "Optional linked project ID." },
projectCode: { type: "string", description: "Optional linked project short code convenience alias." },
name: { type: "string" },
opportunityId: { type: "string" },
baseCurrency: { type: "string" },
status: { type: "string", enum: ["DRAFT", "IN_REVIEW", "APPROVED", "ARCHIVED"] },
versionLabel: { type: "string" },
versionNotes: { type: "string" },
assumptions: { type: "array", items: { type: "object" } },
scopeItems: { type: "array", items: { type: "object" } },
demandLines: { type: "array", items: { type: "object" } },
resourceSnapshots: { type: "array", items: { type: "object" } },
metrics: { type: "array", items: { type: "object" } },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "submit_estimate_version",
description: "Submit an estimate working version for review via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "approve_estimate_version",
description: "Approve a submitted estimate version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "create_estimate_revision",
description: "Create a new working revision from the latest locked estimate version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
sourceVersionId: { type: "string", description: "Optional source version ID." },
label: { type: "string", description: "Optional revision label." },
notes: { type: "string", description: "Optional revision notes." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "create_estimate_export",
description: "Create an estimate export artifact via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
format: { type: "string", enum: ["XLSX", "CSV", "JSON", "SAP", "MMP"], description: "Export format." },
},
required: ["estimateId", "format"],
},
},
},
{
type: "function",
function: {
name: "create_estimate_planning_handoff",
description: "Create planning allocations from an approved estimate version via the real estimate router. Manager/admin role and manageAllocations permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit approved version ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "generate_estimate_weekly_phasing",
description: "Generate weekly phasing for the working estimate version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
startDate: { type: "string", description: "Start date in YYYY-MM-DD." },
endDate: { type: "string", description: "End date in YYYY-MM-DD." },
pattern: { type: "string", enum: ["even", "front_loaded", "back_loaded", "custom"], description: "Distribution pattern." },
},
required: ["estimateId", "startDate", "endDate"],
},
},
},
{
type: "function",
function: {
name: "get_estimate_weekly_phasing",
description: "Get generated weekly phasing for an estimate via the real estimate router. Controller/manager/admin access required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "get_estimate_commercial_terms",
description: "Get estimate commercial terms via the real estimate router. Controller/manager/admin access required.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
},
required: ["estimateId"],
},
},
},
{
type: "function",
function: {
name: "update_estimate_commercial_terms",
description: "Update estimate commercial terms on a working version via the real estimate router. Manager/admin role and manageProjects permission required. Always confirm first.",
parameters: {
type: "object",
properties: {
estimateId: { type: "string", description: "Estimate ID." },
versionId: { type: "string", description: "Optional explicit version ID." },
terms: { type: "object", description: "Commercial terms payload." },
},
required: ["estimateId", "terms"],
},
},
},
// ── ROLES ──
{
type: "function",
function: {
name: "create_role",
description: "Create a new role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Role name" },
description: { type: "string", description: "Optional role description" },
color: { type: "string", description: "Hex color (e.g. #3b82f6). Default: #6b7280" },
},
required: ["name"],
},
},
},
{
type: "function",
function: {
name: "update_role",
description: "Update a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Role ID" },
name: { type: "string", description: "New name" },
description: { type: "string", description: "New description" },
color: { type: "string", description: "New hex color" },
isActive: { type: "boolean", description: "Set active state" },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "delete_role",
description: "Delete a role. Requires manager or admin role plus manageRoles permission. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Role ID" },
},
required: ["id"],
},
},
},
// ── CLIENTS ──
{
type: "function",
function: {
name: "create_client",
description: "Create a new client. Requires manager or admin role. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Client name" },
code: { type: "string", description: "Client code" },
parentId: { type: "string", description: "Optional parent client ID" },
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
tags: { type: "array", items: { type: "string" }, description: "Optional client tags" },
},
required: ["name"],
},
},
},
{
type: "function",
function: {
name: "update_client",
description: "Update a client. Requires manager or admin role. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Client ID" },
name: { type: "string", description: "New name" },
code: { type: "string", description: "New code" },
sortOrder: { type: "integer", description: "New sort order" },
isActive: { type: "boolean", description: "Set active state" },
parentId: { type: "string", description: "Parent client ID; use null to clear" },
tags: { type: "array", items: { type: "string" }, description: "Replacement client tags" },
},
required: ["id"],
},
},
},
// ── ADMIN / CONFIG READ TOOLS ──
{
type: "function",
function: {
name: "list_countries",
description: "List countries including working hours, schedule rules, active flag, and metro cities.",
parameters: {
type: "object",
properties: {
includeInactive: { type: "boolean", description: "Include inactive countries. Default: false." },
search: { type: "string", description: "Optional country code or name search." },
},
},
},
},
{
type: "function",
function: {
name: "get_country",
description: "Get one country with schedule rules, active flag, metro cities, and resource count. Accepts ID, code, or name.",
parameters: {
type: "object",
properties: {
identifier: { type: "string", description: "Country ID, code, or name." },
},
required: ["identifier"],
},
},
},
{
type: "function",
function: {
name: "create_country",
description: "Create a country with daily working hours and optional schedule rules. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
code: { type: "string", description: "ISO country code such as DE or ES." },
name: { type: "string", description: "Country name." },
dailyWorkingHours: { type: "number", description: "Standard daily working hours." },
scheduleRules: {
type: "object",
description: "Optional schedule rule object such as the Spain reduced-hours configuration.",
},
},
required: ["code", "name"],
},
},
},
{
type: "function",
function: {
name: "update_country",
description: "Update a country including working hours, schedule rules, or active state. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Country ID." },
data: {
type: "object",
properties: {
code: { type: "string" },
name: { type: "string" },
dailyWorkingHours: { type: "number" },
scheduleRules: { type: ["object", "null"] },
isActive: { type: "boolean" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "create_metro_city",
description: "Create a metro city for a country. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
countryId: { type: "string", description: "Country ID." },
name: { type: "string", description: "Metro city name." },
},
required: ["countryId", "name"],
},
},
},
{
type: "function",
function: {
name: "update_metro_city",
description: "Rename a metro city. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Metro city ID." },
data: {
type: "object",
properties: {
name: { type: "string" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_metro_city",
description: "Delete a metro city when no resource is assigned to it. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Metro city ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "list_management_levels",
description: "List management level groups and their levels with target percentages.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_utilization_categories",
description: "List utilization categories (cost classification for projects).",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_calculation_rules",
description: "List calculation rules for cost attribution and chargeability.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_effort_rules",
description: "List effort estimation rules with their formulas and conditions.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_experience_multipliers",
description: "List experience multipliers that adjust effort estimates based on seniority.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "list_users",
description: "List all system users via the admin user router, including role and MFA state. Admin role required.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 50" },
},
},
},
},
{
type: "function",
function: {
name: "list_assignable_users",
description: "List lightweight users available for assignment workflows. Manager or admin role required.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_current_user",
description: "Get the authenticated user's own profile, role, and permission overrides.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_dashboard_layout",
description: "Get the authenticated user's saved dashboard widget layout and last update timestamp.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "save_dashboard_layout",
description: "Save the authenticated user's dashboard layout. Always confirm first.",
parameters: {
type: "object",
properties: {
layout: {
type: "array",
description: "Dashboard layout items as stored by the user router.",
items: { type: "object" },
},
},
required: ["layout"],
},
},
},
{
type: "function",
function: {
name: "get_favorite_project_ids",
description: "Get the authenticated user's favorite project IDs.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "toggle_favorite_project",
description: "Add or remove a project from the authenticated user's favorites. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID." },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "get_column_preferences",
description: "Get the authenticated user's saved table column preferences for all supported views.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "set_column_preferences",
description: "Update the authenticated user's table column preferences for one view. Always confirm first.",
parameters: {
type: "object",
properties: {
view: {
type: "string",
enum: ["resources", "projects", "allocations", "vacations", "roles", "users", "blueprints"],
description: "View key to update.",
},
visible: {
type: "array",
items: { type: "string" },
description: "Visible column IDs.",
},
sort: {
type: ["object", "null"],
properties: {
field: { type: "string" },
dir: { type: "string", enum: ["asc", "desc"] },
},
description: "Sort state. Use null to clear it.",
},
rowOrder: {
type: ["array", "null"],
items: { type: "string" },
description: "Optional row order. Use null to clear it.",
},
},
required: ["view"],
},
},
},
{
type: "function",
function: {
name: "generate_totp_secret",
description: "Generate a new MFA TOTP secret and provisioning URI for the authenticated user. Always confirm first. The secret is sensitive.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "verify_and_enable_totp",
description: "Verify a 6-digit MFA TOTP token and enable MFA for the authenticated user. Always confirm first.",
parameters: {
type: "object",
properties: {
token: { type: "string", description: "6-digit TOTP token." },
},
required: ["token"],
},
},
},
{
type: "function",
function: {
name: "get_mfa_status",
description: "Get the authenticated user's MFA status.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_active_user_count",
description: "Get the number of users active in the last five minutes. Admin role required.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "create_user",
description: "Create a new system user and auto-link a matching resource by email when possible. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
email: { type: "string", description: "User email address." },
name: { type: "string", description: "Display name." },
systemRole: { type: "string", enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"], description: "Initial system role." },
password: { type: "string", description: "Initial password, minimum 8 characters." },
},
required: ["email", "name", "password"],
},
},
},
{
type: "function",
function: {
name: "set_user_password",
description: "Reset a user's password. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
password: { type: "string", description: "New password, minimum 8 characters." },
},
required: ["userId", "password"],
},
},
},
{
type: "function",
function: {
name: "update_user_role",
description: "Change a user's system role. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "User ID." },
systemRole: { type: "string", enum: ["ADMIN", "MANAGER", "CONTROLLER", "USER", "VIEWER"] },
},
required: ["id", "systemRole"],
},
},
},
{
type: "function",
function: {
name: "update_user_name",
description: "Rename a user. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "User ID." },
name: { type: "string", description: "New display name." },
},
required: ["id", "name"],
},
},
},
{
type: "function",
function: {
name: "link_user_resource",
description: "Link or unlink a user to a resource. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
resourceId: { type: ["string", "null"], description: "Resource ID or null to unlink." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "auto_link_users_by_email",
description: "Auto-link all users without a resource to matching resources by email. Admin role required. Always confirm first.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "set_user_permissions",
description: "Set explicit permission overrides for a user. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
overrides: {
type: ["object", "null"],
properties: {
granted: { type: "array", items: { type: "string" } },
denied: { type: "array", items: { type: "string" } },
chapterIds: { type: "array", items: { type: "string" } },
},
description: "Permission override object or null to clear.",
},
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "reset_user_permissions",
description: "Reset a user's permission overrides back to role defaults. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "get_effective_user_permissions",
description: "Get a user's resolved permissions, role, and explicit overrides. Admin role required.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "disable_user_totp",
description: "Disable MFA TOTP for a user as an admin override. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "User ID." },
},
required: ["userId"],
},
},
},
{
type: "function",
function: {
name: "list_notifications",
description: "List recent notifications for the current user.",
parameters: {
type: "object",
properties: {
unreadOnly: { type: "boolean", description: "Only show unread. Default: false" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "mark_notification_read",
description: "Mark one notification as read, or all unread notifications when no notificationId is provided. Always confirm first.",
parameters: {
type: "object",
properties: {
notificationId: { type: "string", description: "Notification ID. Omit to mark all unread notifications as read." },
},
},
},
},
{
type: "function",
function: {
name: "get_unread_notification_count",
description: "Count unread notifications for the current user.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "create_notification",
description: "Create a notification or task-style notification for a specific user. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "Target user ID." },
type: { type: "string", description: "Notification type code." },
title: { type: "string", description: "Title." },
body: { type: "string", description: "Optional body text." },
entityId: { type: "string", description: "Optional linked entity ID." },
entityType: { type: "string", description: "Optional linked entity type." },
category: { type: "string", enum: ["NOTIFICATION", "REMINDER", "TASK", "APPROVAL"] },
priority: { type: "string", enum: ["LOW", "NORMAL", "HIGH", "URGENT"] },
link: { type: "string", description: "Optional deep link." },
taskStatus: { type: "string", enum: ["OPEN", "IN_PROGRESS", "DONE", "DISMISSED"] },
taskAction: { type: "string", description: "Optional machine-readable task action." },
assigneeId: { type: "string", description: "Optional assignee user ID." },
dueDate: { type: "string", format: "date-time", description: "Optional due date." },
channel: { type: "string", enum: ["in_app", "email", "both"] },
senderId: { type: "string", description: "Optional sender override." },
},
required: ["userId", "type", "title"],
},
},
},
// ── DASHBOARD DETAIL ──
{
type: "function",
function: {
name: "get_dashboard_detail",
description: "Get detailed dashboard data: peak allocation times, top-value resources, demand pipeline, chargeability overview.",
parameters: {
type: "object",
properties: {
section: {
type: "string",
description: "Which section: peak_times, top_resources, demand_pipeline, chargeability_overview, or all",
},
},
},
},
},
// ── PROJECT MANAGEMENT ──
{
type: "function",
function: {
name: "delete_project",
description: "Delete a project. Only DRAFT projects can be deleted. Requires manageProjects permission. Always confirm first.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
},
required: ["projectId"],
},
},
},
// ── ORG UNIT MANAGEMENT ──
{
type: "function",
function: {
name: "create_org_unit",
description: "Create a new organizational unit. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Org unit name" },
shortName: { type: "string", description: "Short name/code" },
level: { type: "integer", description: "Level (5, 6, or 7)" },
parentId: { type: "string", description: "Parent org unit ID (optional)" },
sortOrder: { type: "integer", description: "Sort order. Default: 0" },
},
required: ["name", "level"],
},
},
},
{
type: "function",
function: {
name: "update_org_unit",
description: "Update an organizational unit. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Org unit ID" },
name: { type: "string", description: "New name" },
shortName: { type: "string", description: "New short name" },
sortOrder: { type: "integer", description: "New sort order" },
isActive: { type: "boolean", description: "Set active state" },
parentId: { type: "string", description: "Parent org unit ID; use null to clear" },
},
required: ["id"],
},
},
},
// ── COVER ART ──
{
type: "function",
function: {
name: "generate_project_cover",
description: "Generate an AI cover art image for a project. Uses the configured image provider (DALL-E or Google Gemini). The image will be stored as the project's cover. Requires manageProjects permission.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
prompt: { type: "string", description: "Optional custom prompt for the AI image generation (e.g. 'futuristic car in neon cityscape'). If not provided, a default automotive/CGI prompt is used based on the project name." },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "remove_project_cover",
description: "Remove the cover art image from a project. Requires manageProjects permission.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
},
required: ["projectId"],
},
},
},
// ── TASK MANAGEMENT ──
{
type: "function",
function: {
name: "list_tasks",
description: "List tasks and approvals for the current user via the real notification router, optionally including tasks assigned to them.",
parameters: {
type: "object",
properties: {
status: { type: "string", enum: ["OPEN", "IN_PROGRESS", "DONE", "DISMISSED"], description: "Optional status filter." },
includeAssigned: { type: "boolean", description: "Include tasks where the current user is assignee as well as owner. Default: true." },
limit: { type: "integer", description: "Max results. Default: 20." },
},
},
},
},
{
type: "function",
function: {
name: "get_task_counts",
description: "Get open, in-progress, done, dismissed, and overdue task counts for the current user.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_task_detail",
description: "Get details of a specific task/notification including linked entity information.",
parameters: {
type: "object",
properties: {
taskId: { type: "string", description: "Notification/task ID" },
},
required: ["taskId"],
},
},
},
{
type: "function",
function: {
name: "update_task_status",
description: "Update the status of a task. Mark as IN_PROGRESS, DONE, or DISMISSED.",
parameters: {
type: "object",
properties: {
taskId: { type: "string", description: "Task/notification ID" },
status: { type: "string", enum: ["OPEN", "IN_PROGRESS", "DONE", "DISMISSED"], description: "New status" },
},
required: ["taskId", "status"],
},
},
},
{
type: "function",
function: {
name: "execute_task_action",
description: "Execute the machine-readable action associated with a task. For example: approve a vacation, confirm an assignment, etc. The action is encoded in the task's taskAction field.",
parameters: {
type: "object",
properties: {
taskId: { type: "string", description: "Task/notification ID containing the action to execute" },
},
required: ["taskId"],
},
},
},
{
type: "function",
function: {
name: "create_reminder",
description: "Create a personal reminder for the current user via the real notification router. Always confirm first.",
parameters: {
type: "object",
properties: {
title: { type: "string", description: "Reminder title" },
body: { type: "string", description: "Optional details" },
remindAt: { type: "string", format: "date-time", description: "When to remind (ISO 8601 datetime)" },
recurrence: { type: "string", enum: ["daily", "weekly", "monthly"], description: "Optional recurrence pattern" },
entityId: { type: "string", description: "Optional: linked entity ID (project, resource, etc.)" },
entityType: { type: "string", description: "Optional: entity type (project, resource, vacation, etc.)" },
link: { type: "string", description: "Optional deep link." },
},
required: ["title", "remindAt"],
},
},
},
{
type: "function",
function: {
name: "list_reminders",
description: "List personal reminders for the current user via the real notification router.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 20." },
},
},
},
},
{
type: "function",
function: {
name: "update_reminder",
description: "Update a personal reminder via the real notification router. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Reminder notification ID." },
title: { type: "string", description: "Optional reminder title." },
body: { type: "string", description: "Optional reminder body." },
remindAt: { type: "string", format: "date-time", description: "Optional reminder timestamp." },
recurrence: { type: ["string", "null"], enum: ["daily", "weekly", "monthly", null], description: "Optional recurrence update. Use null to clear recurrence." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "delete_reminder",
description: "Delete a personal reminder via the real notification router. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Reminder notification ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "create_task_for_user",
description: "Create a task for a specific user via the real notification router. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
userId: { type: "string", description: "Target user ID" },
title: { type: "string", description: "Task title" },
body: { type: "string", description: "Task description" },
priority: { type: "string", enum: ["LOW", "NORMAL", "HIGH", "URGENT"], description: "Priority (default NORMAL)" },
dueDate: { type: "string", format: "date-time", description: "Optional due date (ISO 8601)" },
taskAction: { type: "string", description: "Optional machine-readable action (format: action_name:entity_id)" },
entityId: { type: "string", description: "Optional linked entity ID" },
entityType: { type: "string", description: "Optional entity type" },
link: { type: "string", description: "Optional deep link." },
channel: { type: "string", enum: ["in_app", "email", "both"], description: "Delivery channel. Default: in_app." },
},
required: ["userId", "title"],
},
},
},
{
type: "function",
function: {
name: "assign_task",
description: "Assign or reassign a task to another user via the real notification router. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Task notification ID." },
assigneeId: { type: "string", description: "User ID to assign." },
},
required: ["id", "assigneeId"],
},
},
},
{
type: "function",
function: {
name: "send_broadcast",
description: "Create and send a broadcast notification via the real notification router. Manager or admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
title: { type: "string", description: "Notification title" },
body: { type: "string", description: "Notification body" },
targetType: { type: "string", enum: ["user", "role", "project", "orgUnit", "all"], description: "Target audience type" },
targetValue: { type: "string", description: "Target value: user ID, role name (ADMIN/MANAGER/CONTROLLER/USER/VIEWER), project ID, or org unit ID" },
category: { type: "string", enum: ["NOTIFICATION", "REMINDER", "TASK", "APPROVAL"], description: "Broadcast category. Default: NOTIFICATION." },
priority: { type: "string", enum: ["LOW", "NORMAL", "HIGH", "URGENT"], description: "Priority (default NORMAL)" },
channel: { type: "string", enum: ["in_app", "email", "both"], description: "Delivery channel (default in_app)" },
link: { type: "string", description: "Optional deep-link URL" },
scheduledAt: { type: "string", format: "date-time", description: "Optional scheduled send timestamp." },
taskAction: { type: "string", description: "Optional machine-readable task action for task-like broadcasts." },
dueDate: { type: "string", format: "date-time", description: "Optional due date for task-like broadcasts." },
},
required: ["title", "targetType"],
},
},
},
{
type: "function",
function: {
name: "list_broadcasts",
description: "List notification broadcasts via the real notification router. Manager or admin role required.",
parameters: {
type: "object",
properties: {
limit: { type: "integer", description: "Max results. Default: 20." },
},
},
},
},
{
type: "function",
function: {
name: "get_broadcast_detail",
description: "Get one notification broadcast via the real notification router. Manager or admin role required.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Broadcast ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "delete_notification",
description: "Delete one of the current user's own notifications via the real notification router. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Notification ID." },
},
required: ["id"],
},
},
},
// ── INSIGHTS & ANOMALIES ──
{
type: "function",
function: {
name: "detect_anomalies",
description: "Detect anomalies across all active projects: budget burn rate issues, staffing gaps, utilization outliers, and timeline overruns.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_skill_gaps",
description: "Analyze skill supply vs demand across all active projects. Returns which skills are in short supply relative to demand requirements.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_project_health",
description: "Get health scores for all active projects based on budget utilization, staffing completeness, and timeline status.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_budget_forecast",
description: "Get budget utilization and burn rate per active project. Shows total budget, spent, remaining, and whether burn is ahead or behind schedule.",
parameters: { type: "object", properties: {} },
},
},
{
type: "function",
function: {
name: "get_insights_summary",
description: "Get a summary of anomaly counts by category (budget, staffing, timeline, utilization) plus critical count.",
parameters: { type: "object", properties: {} },
},
},
// ── REPORTS & COMMENTS ──
{
type: "function",
function: {
name: "run_report",
description: "Run a dynamic report query on resources, projects, assignments, or resource-month rows with flexible column selection and filtering.",
parameters: {
type: "object",
properties: {
entity: {
type: "string",
enum: ["resource", "project", "assignment", "resource_month"],
description: "Entity type to query",
},
columns: {
type: "array",
items: { type: "string" },
description: "Column keys to include (e.g. 'displayName', 'chapter', 'country.name')",
},
filters: {
type: "array",
items: {
type: "object",
properties: {
field: { type: "string", description: "Field to filter on" },
op: { type: "string", enum: ["eq", "neq", "gt", "lt", "gte", "lte", "contains", "in"], description: "Filter operator" },
value: { type: "string", description: "Filter value (string)" },
},
required: ["field", "op", "value"],
},
description: "Filters to apply",
},
periodMonth: {
type: "string",
description: "Required for resource_month reports. Format: YYYY-MM",
},
limit: { type: "integer", description: "Max results. Default: 50" },
},
required: ["entity", "columns"],
},
},
},
{
type: "function",
function: {
name: "list_comments",
description: "List comments (with replies) for a specific entity such as an estimate, scope item, or demand line.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Entity type (e.g. 'estimate', 'estimate_version', 'scope_item', 'demand_line')" },
entityId: { type: "string", description: "Entity ID" },
},
required: ["entityType", "entityId"],
},
},
},
{
type: "function",
function: {
name: "lookup_rate",
description: "Find the best matching rate card line for given criteria (client, chapter, management level, role, seniority).",
parameters: {
type: "object",
properties: {
clientId: { type: "string", description: "Client ID to find rate card for" },
chapter: { type: "string", description: "Chapter to match" },
managementLevelId: { type: "string", description: "Management level ID to match" },
roleName: { type: "string", description: "Role name to match" },
seniority: { type: "string", description: "Seniority level to match" },
},
},
},
},
// ── SCENARIO & AI ──
{
type: "function",
function: {
name: "simulate_scenario",
description: "Run a read-only what-if staffing simulation for a project. Shows cost/hours/utilization impact of adding, removing, or changing resource assignments without persisting changes.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
changes: {
type: "array",
items: {
type: "object",
properties: {
assignmentId: { type: "string", description: "Existing assignment ID to modify (omit for new)" },
resourceId: { type: "string", description: "Resource ID" },
roleId: { type: "string", description: "Role ID" },
startDate: { type: "string", description: "Start date YYYY-MM-DD" },
endDate: { type: "string", description: "End date YYYY-MM-DD" },
hoursPerDay: { type: "number", description: "Hours per day" },
remove: { type: "boolean", description: "Set true to remove an existing assignment" },
},
required: ["startDate", "endDate", "hoursPerDay"],
},
description: "Array of staffing changes to simulate",
},
},
required: ["projectId", "changes"],
},
},
},
{
type: "function",
function: {
name: "generate_project_narrative",
description: "Generate an AI-powered executive narrative for a project covering budget, staffing, timeline risk, and action items. Requires AI to be configured.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID" },
},
required: ["projectId"],
},
},
},
{
type: "function",
function: {
name: "create_comment",
description: "Add a comment to an entity (estimate, scope item, demand line, etc.). Supports @mentions. Always confirm with the user first.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Entity type (e.g. 'estimate', 'estimate_version', 'scope_item')" },
entityId: { type: "string", description: "Entity ID" },
body: { type: "string", description: "Comment body text. Use @[Name](userId) for mentions." },
},
required: ["entityType", "entityId", "body"],
},
},
},
{
type: "function",
function: {
name: "resolve_comment",
description: "Mark a comment as resolved (or unresolve it). Only the comment author or an admin can do this.",
parameters: {
type: "object",
properties: {
commentId: { type: "string", description: "Comment ID to resolve" },
resolved: { type: "boolean", description: "Set to true to resolve, false to unresolve. Default: true" },
},
required: ["commentId"],
},
},
},
{
type: "function",
function: {
name: "query_change_history",
description: "Search the audit history for changes to projects, resources, allocations, vacations, or any entity. Reuses the real audit log list API. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Filter by entity type (e.g. 'Project', 'Resource', 'Allocation', 'Vacation', 'Role', 'Estimate')" },
search: { type: "string", description: "Search in entity name or summary text" },
userId: { type: "string", description: "Filter by user ID who made the change" },
daysBack: { type: "integer", description: "How many days back to search. Default: 7" },
action: { type: "string", description: "Filter by action type: CREATE, UPDATE, DELETE, SHIFT, IMPORT" },
limit: { type: "integer", description: "Max results. Default: 20" },
},
},
},
},
{
type: "function",
function: {
name: "get_entity_timeline",
description: "Get the audit history for a specific entity (project, resource, etc.) via the real audit API. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Entity type (e.g. 'Project', 'Resource', 'Allocation')" },
entityId: { type: "string", description: "Entity ID" },
limit: { type: "integer", description: "Max results. Default: 50" },
},
required: ["entityType", "entityId"],
},
},
},
{
type: "function",
function: {
name: "export_resources_csv",
description: "Export the current active resource list as CSV via the real import/export router. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "export_projects_csv",
description: "Export the current project list as CSV via the real import/export router. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "import_csv_data",
description: "Import CSV-style row data for resources, projects, or allocations via the real import/export router. Requires manager/admin, importData permission, and confirmation.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", enum: ["resources", "projects", "allocations"], description: "Import target entity type." },
rows: {
type: "array",
description: "CSV rows already parsed to key/value objects.",
items: {
type: "object",
additionalProperties: { type: "string" },
},
},
dryRun: { type: "boolean", description: "Validate only without persisting changes. Default: true." },
},
required: ["entityType", "rows"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_import_batches",
description: "List Dispo import batches with pagination and optional status filter via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
status: { type: "string", description: "Optional batch status filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
},
},
},
{
type: "function",
function: {
name: "get_dispo_import_batch",
description: "Get one Dispo import batch including staged record counters via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Import batch ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "stage_dispo_import_batch",
description: "Stage a Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
planningWorkbookPath: { type: "string", description: "Filesystem path to the planning workbook." },
referenceWorkbookPath: { type: "string", description: "Filesystem path to the reference workbook." },
chargeabilityWorkbookPath: { type: "string", description: "Filesystem path to the chargeability workbook." },
costWorkbookPath: { type: "string", description: "Optional filesystem path to the cost workbook." },
rosterWorkbookPath: { type: "string", description: "Optional filesystem path to the roster workbook." },
notes: { type: "string", description: "Optional import notes." },
},
required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"],
},
},
},
{
type: "function",
function: {
name: "validate_dispo_import_batch",
description: "Validate a Dispo import batch readiness check via the real dispo router without committing anything. Admin role required.",
parameters: {
type: "object",
properties: {
planningWorkbookPath: { type: "string", description: "Filesystem path to the planning workbook." },
referenceWorkbookPath: { type: "string", description: "Filesystem path to the reference workbook." },
chargeabilityWorkbookPath: { type: "string", description: "Filesystem path to the chargeability workbook." },
costWorkbookPath: { type: "string", description: "Optional filesystem path to the cost workbook." },
rosterWorkbookPath: { type: "string", description: "Optional filesystem path to the roster workbook." },
importBatchId: { type: "string", description: "Optional existing staged import batch ID." },
notes: { type: "string", description: "Optional import notes." },
},
required: ["planningWorkbookPath", "referenceWorkbookPath", "chargeabilityWorkbookPath"],
},
},
},
{
type: "function",
function: {
name: "cancel_dispo_import_batch",
description: "Cancel a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Import batch ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_resources",
description: "List staged Dispo resources for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
status: { type: "string", description: "Optional staged record status filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_projects",
description: "List staged Dispo projects for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
status: { type: "string", description: "Optional staged record status filter." },
isTbd: { type: "boolean", description: "Optional TBD-project filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_assignments",
description: "List staged Dispo assignments for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
status: { type: "string", description: "Optional staged record status filter." },
resourceExternalId: { type: "string", description: "Optional resource external ID filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_vacations",
description: "List staged Dispo vacations for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
resourceExternalId: { type: "string", description: "Optional resource external ID filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "list_dispo_staged_unresolved_records",
description: "List staged unresolved Dispo records for one import batch via the real dispo router. Admin role required.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
recordType: { type: "string", description: "Optional unresolved record type filter." },
limit: { type: "integer", description: "Max results. Default: 50, max: 200." },
cursor: { type: "string", description: "Optional pagination cursor." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "resolve_dispo_staged_record",
description: "Resolve one staged Dispo record via the real dispo router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Staged record ID." },
recordType: { type: "string", description: "Staged record type." },
action: { type: "string", enum: ["APPROVE", "REJECT", "SKIP"], description: "Resolution action." },
},
required: ["id", "recordType", "action"],
},
},
},
{
type: "function",
function: {
name: "commit_dispo_import_batch",
description: "Commit a staged Dispo import batch via the real dispo router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
importBatchId: { type: "string", description: "Import batch ID." },
allowTbdUnresolved: { type: "boolean", description: "Allow unresolved TBD projects during commit." },
importTbdProjects: { type: "boolean", description: "Whether TBD projects should be imported." },
},
required: ["importBatchId"],
},
},
},
{
type: "function",
function: {
name: "get_system_settings",
description: "Get sanitized system settings through the real settings router. Admin role required.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "update_system_settings",
description: "Update system settings through the real settings router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
aiProvider: { type: "string", enum: ["openai", "azure"] },
azureOpenAiEndpoint: { type: "string" },
azureOpenAiDeployment: { type: "string" },
azureOpenAiApiKey: { type: "string" },
azureApiVersion: { type: "string" },
aiMaxCompletionTokens: { type: "integer" },
aiTemperature: { type: "number" },
aiSummaryPrompt: { type: "string" },
scoreWeights: { type: "object" },
scoreVisibleRoles: { type: "array", items: { type: "string" } },
smtpHost: { type: "string" },
smtpPort: { type: "integer" },
smtpUser: { type: "string" },
smtpPassword: { type: "string" },
smtpFrom: { type: "string" },
smtpTls: { type: "boolean" },
anonymizationEnabled: { type: "boolean" },
anonymizationDomain: { type: "string" },
anonymizationSeed: { type: "string" },
anonymizationMode: { type: "string", enum: ["global"] },
azureDalleDeployment: { type: "string" },
azureDalleEndpoint: { type: "string" },
azureDalleApiKey: { type: "string" },
geminiApiKey: { type: "string" },
geminiModel: { type: "string" },
imageProvider: { type: "string", enum: ["dalle", "gemini"] },
vacationDefaultDays: { type: "integer" },
timelineUndoMaxSteps: { type: "integer" },
},
},
},
},
{
type: "function",
function: {
name: "test_ai_connection",
description: "Run the real AI connection test from system settings. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "test_smtp_connection",
description: "Run the real SMTP connection test from system settings. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "test_gemini_connection",
description: "Run the real Gemini connection test from system settings. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "get_ai_configured",
description: "Get whether AI is configured for the current system via the real settings router. Admin role required.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "list_system_role_configs",
description: "List system role configuration defaults via the real system-role-config router. Admin role required.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "update_system_role_config",
description: "Update one system role configuration via the real system-role-config router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
role: { type: "string", description: "System role key." },
label: { type: "string", description: "Optional role label." },
description: { type: "string", description: "Optional role description." },
color: { type: "string", description: "Optional role color." },
defaultPermissions: { type: "array", items: { type: "string" }, description: "Optional default permission set." },
},
required: ["role"],
},
},
},
{
type: "function",
function: {
name: "list_webhooks",
description: "List webhooks via the real webhook router. Secrets are masked in assistant responses. Admin role required.",
parameters: {
type: "object",
properties: {},
},
},
},
{
type: "function",
function: {
name: "get_webhook",
description: "Get one webhook via the real webhook router. Secrets are masked in assistant responses. Admin role required.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Webhook ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "create_webhook",
description: "Create a webhook via the real webhook router. Secrets are masked in assistant responses. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
name: { type: "string", description: "Webhook name." },
url: { type: "string", description: "Webhook target URL." },
secret: { type: "string", description: "Optional webhook signing secret." },
events: { type: "array", items: { type: "string" }, description: "Subscribed webhook events." },
isActive: { type: "boolean", description: "Whether the webhook is active. Default: true." },
},
required: ["name", "url", "events"],
},
},
},
{
type: "function",
function: {
name: "update_webhook",
description: "Update a webhook via the real webhook router. Secrets are masked in assistant responses. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Webhook ID." },
data: {
type: "object",
properties: {
name: { type: "string" },
url: { type: "string" },
secret: { type: "string" },
events: { type: "array", items: { type: "string" } },
isActive: { type: "boolean" },
},
},
},
required: ["id", "data"],
},
},
},
{
type: "function",
function: {
name: "delete_webhook",
description: "Delete a webhook via the real webhook router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Webhook ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "test_webhook",
description: "Send a real test payload to a webhook via the real webhook router. Admin role required. Always confirm first.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Webhook ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "list_audit_log_entries",
description: "List audit log entries with full audit-router filters and cursor pagination. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
entityType: { type: "string", description: "Optional entity type filter." },
entityId: { type: "string", description: "Optional entity ID filter." },
userId: { type: "string", description: "Optional user ID filter." },
action: { type: "string", description: "Optional action filter such as CREATE, UPDATE, DELETE, SHIFT, IMPORT." },
source: { type: "string", description: "Optional source filter such as ui or assistant." },
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
search: { type: "string", description: "Optional case-insensitive search across entity name, summary, and entity type." },
limit: { type: "integer", description: "Max results. Default: 50, max: 100." },
cursor: { type: "string", description: "Optional pagination cursor (last seen audit entry ID)." },
},
},
},
},
{
type: "function",
function: {
name: "get_audit_log_entry",
description: "Get one audit log entry including the full changes payload. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
id: { type: "string", description: "Audit log entry ID." },
},
required: ["id"],
},
},
},
{
type: "function",
function: {
name: "get_audit_log_timeline",
description: "Get audit log entries grouped by day for a time window. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
limit: { type: "integer", description: "Max entries. Default: 200, max: 500." },
},
},
},
},
{
type: "function",
function: {
name: "get_audit_activity_summary",
description: "Get audit activity totals by entity type, action, and user for a date range. Controller/manager/admin roles only.",
parameters: {
type: "object",
properties: {
startDate: { type: "string", description: "Optional start date in YYYY-MM-DD." },
endDate: { type: "string", description: "Optional end date in YYYY-MM-DD." },
},
},
},
},
{
type: "function",
function: {
name: "get_shoring_ratio",
description: "Get the onshore/offshore staffing ratio for a project. Higher offshore is better (cost-efficient). The threshold is the MINIMUM offshore target. Shows country breakdown and whether the target is met.",
parameters: {
type: "object",
properties: {
projectId: { type: "string", description: "Project ID or short code" },
},
required: ["projectId"],
},
},
},
];
// ─── Helpers ────────────────────────────────────────────────────────────────
/** Resolve a responsible person name against existing resources. Returns the exact displayName or an error object. */
async function resolveResponsiblePerson(
name: string,
ctx: ToolContext,
): Promise<{ displayName: string } | { error: string }> {
const caller = createResourceCaller(createScopedCallerContext(ctx));
const result = await caller.resolveResponsiblePersonName({ name });
if (result.status === "resolved") {
return { displayName: result.displayName };
}
if (result.status === "ambiguous" || result.status === "missing") {
return { error: result.message };
}
return { error: `Unable to resolve responsible person: ${name}` };
}
// ─── Tool Executors ─────────────────────────────────────────────────────────
const executors = {
async search_resources(params: {
query?: string; country?: string; metroCity?: string;
orgUnit?: string; roleName?: string;
isActive?: boolean; limit?: number;
}, ctx: ToolContext) {
const caller = createResourceCaller(createScopedCallerContext(ctx));
return caller.listSummariesDetail({
search: params.query,
country: params.country,
metroCity: params.metroCity,
orgUnit: params.orgUnit,
roleName: params.roleName,
isActive: params.isActive ?? true,
limit: Math.min(params.limit ?? 50, 100),
});
},
async get_resource(params: { identifier: string }, ctx: ToolContext) {
const caller = createResourceCaller(createScopedCallerContext(ctx));
return caller.getByIdentifierDetail({ identifier: params.identifier });
},
async search_projects(params: { query?: string; status?: string; limit?: number }, ctx: ToolContext) {
const caller = createProjectCaller(createScopedCallerContext(ctx));
return caller.searchSummariesDetail({
search: params.query,
status: params.status as import("@capakraken/shared").ProjectStatus | undefined,
limit: Math.min(params.limit ?? 20, 50),
});
},
async get_project(params: { identifier: string }, ctx: ToolContext) {
const caller = createProjectCaller(createScopedCallerContext(ctx));
let project;
try {
project = await caller.getByIdentifierDetail({ identifier: params.identifier });
} catch {
return { error: `Project not found: ${params.identifier}` };
}
return project;
},
async find_best_project_resource(params: {
projectIdentifier: string;
startDate?: string;
endDate?: string;
durationDays?: number;
minHoursPerDay?: number;
rankingMode?: "lowest_lcr" | "highest_remaining_hours_per_day" | "highest_remaining_hours";
chapter?: string;
roleName?: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
assertPermission(ctx, PermissionKey.VIEW_COSTS);
const project = await resolveProjectIdentifier(ctx, params.projectIdentifier);
if ("error" in project) {
return project;
}
const caller = createStaffingCaller(createScopedCallerContext(ctx));
return caller.getBestProjectResourceDetail({
projectId: project.id,
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
...(params.minHoursPerDay !== undefined ? { minHoursPerDay: params.minHoursPerDay } : {}),
...(params.rankingMode ? { rankingMode: params.rankingMode } : {}),
...(params.chapter ? { chapter: params.chapter } : {}),
...(params.roleName ? { roleName: params.roleName } : {}),
});
},
async get_timeline_entries_view(params: {
startDate?: string;
endDate?: string;
durationDays?: number;
resourceIds?: string[];
projectIds?: string[];
clientIds?: string[];
chapters?: string[];
eids?: string[];
countryCodes?: string[];
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getEntriesDetail({ ...params });
},
async get_timeline_holiday_overlays(params: {
startDate?: string;
endDate?: string;
durationDays?: number;
resourceIds?: string[];
projectIds?: string[];
clientIds?: string[];
chapters?: string[];
eids?: string[];
countryCodes?: string[];
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getHolidayOverlayDetail({ ...params });
},
async get_project_timeline_context(params: {
projectIdentifier: string;
startDate?: string;
endDate?: string;
durationDays?: number;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const project = await resolveProjectIdentifier(ctx, params.projectIdentifier);
if ("error" in project) {
return project;
}
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getProjectContextDetail({
projectId: project.id,
...(params.startDate ? { startDate: params.startDate } : {}),
...(params.endDate ? { endDate: params.endDate } : {}),
...(params.durationDays !== undefined ? { durationDays: params.durationDays } : {}),
});
},
async preview_project_shift(params: {
projectIdentifier: string;
newStartDate: string;
newEndDate: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const project = await resolveProjectIdentifier(ctx, params.projectIdentifier);
if ("error" in project) {
return project;
}
const caller = createTimelineCaller(createScopedCallerContext(ctx));
return caller.getShiftPreviewDetail({
projectId: project.id,
newStartDate: parseIsoDate(params.newStartDate, "newStartDate"),
newEndDate: parseIsoDate(params.newEndDate, "newEndDate"),
});
},
async update_timeline_allocation_inline(params: {
allocationId: string;
hoursPerDay?: number;
startDate?: string;
endDate?: string;
includeSaturday?: boolean;
role?: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createTimelineCaller(createScopedCallerContext(ctx));
const updated = await caller.updateAllocationInline({
allocationId: params.allocationId,
...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
...(params.includeSaturday !== undefined ? { includeSaturday: params.includeSaturday } : {}),
...(params.role !== undefined ? { role: params.role } : {}),
});
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Updated timeline allocation ${updated.id}.`,
allocation: {
id: updated.id,
projectId: updated.projectId,
resourceId: updated.resourceId ?? null,
startDate: fmtDate(updated.startDate),
endDate: fmtDate(updated.endDate),
hoursPerDay: updated.hoursPerDay,
role: updated.role ?? null,
status: updated.status,
},
};
},
async apply_timeline_project_shift(params: {
projectIdentifier: string;
newStartDate: string;
newEndDate: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const project = await resolveProjectIdentifier(ctx, params.projectIdentifier);
if ("error" in project) {
return project;
}
const newStartDate = parseIsoDate(params.newStartDate, "newStartDate");
const newEndDate = parseIsoDate(params.newEndDate, "newEndDate");
const caller = createTimelineCaller(createScopedCallerContext(ctx));
const result = await caller.applyShift({
projectId: project.id,
newStartDate,
newEndDate,
});
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Shifted project ${project.shortCode ?? project.name ?? project.id} to ${fmtDate(newStartDate)} - ${fmtDate(newEndDate)}.`,
project: {
id: result.project.id,
startDate: fmtDate(result.project.startDate),
endDate: fmtDate(result.project.endDate),
},
validation: result.validation,
};
},
async quick_assign_timeline_resource(params: {
resourceIdentifier: string;
projectIdentifier: string;
startDate: string;
endDate: string;
hoursPerDay?: number;
role?: string;
roleId?: string;
status?: AllocationStatus;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, params.resourceIdentifier),
resolveProjectIdentifier(ctx, params.projectIdentifier),
]);
if ("error" in resource) {
return resource;
}
if ("error" in project) {
return project;
}
const caller = createTimelineCaller(createScopedCallerContext(ctx));
const allocation = await caller.quickAssign({
resourceId: resource.id,
projectId: project.id,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
...(params.hoursPerDay !== undefined ? { hoursPerDay: params.hoursPerDay } : {}),
...(params.role !== undefined ? { role: params.role } : {}),
...(params.roleId !== undefined ? { roleId: params.roleId } : {}),
...(params.status !== undefined ? { status: params.status } : {}),
});
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Quick-assigned ${resource.displayName} to ${project.name} (${project.shortCode ?? project.id}).`,
allocation: {
id: allocation.id,
projectId: allocation.projectId,
resourceId: allocation.resourceId ?? null,
startDate: fmtDate(toDate(allocation.startDate)),
endDate: fmtDate(toDate(allocation.endDate)),
hoursPerDay: allocation.hoursPerDay,
role: allocation.role ?? null,
status: allocation.status,
},
};
},
async batch_quick_assign_timeline_resources(params: {
assignments: Array<{
resourceIdentifier: string;
projectIdentifier: string;
startDate: string;
endDate: string;
hoursPerDay?: number;
role?: string;
status?: AllocationStatus;
}>;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const resolvedAssignments = await Promise.all(params.assignments.map(async (assignment, index) => {
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, assignment.resourceIdentifier),
resolveProjectIdentifier(ctx, assignment.projectIdentifier),
]);
if ("error" in resource) {
throw new Error(`assignments[${index}].resourceIdentifier: ${resource.error}`);
}
if ("error" in project) {
throw new Error(`assignments[${index}].projectIdentifier: ${project.error}`);
}
return {
resourceId: resource.id,
projectId: project.id,
startDate: parseIsoDate(assignment.startDate, `assignments[${index}].startDate`),
endDate: parseIsoDate(assignment.endDate, `assignments[${index}].endDate`),
...(assignment.hoursPerDay !== undefined ? { hoursPerDay: assignment.hoursPerDay } : {}),
...(assignment.role !== undefined ? { role: assignment.role } : {}),
...(assignment.status !== undefined ? { status: assignment.status } : {}),
};
}));
const caller = createTimelineCaller(createScopedCallerContext(ctx));
const result = await caller.batchQuickAssign({
assignments: resolvedAssignments,
});
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Created ${result.count} timeline quick-assignment(s).`,
count: result.count,
};
},
async batch_shift_timeline_allocations(params: {
allocationIds: string[];
daysDelta: number;
mode?: "move" | "resize-start" | "resize-end";
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.MANAGE_ALLOCATIONS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createTimelineCaller(createScopedCallerContext(ctx));
const result = await caller.batchShiftAllocations({
allocationIds: params.allocationIds,
daysDelta: params.daysDelta,
...(params.mode !== undefined ? { mode: params.mode } : {}),
});
return {
__action: "invalidate",
scope: ["allocation", "timeline", "project"],
success: true,
message: `Shifted ${result.count} allocation(s) by ${params.daysDelta} day(s).`,
count: result.count,
};
},
async list_allocations(params: {
resourceId?: string; projectId?: string;
resourceName?: string; projectCode?: string;
status?: string; limit?: number;
}, ctx: ToolContext) {
const caller = createAllocationCaller(createScopedCallerContext(ctx));
const status = params.status && Object.values(AllocationStatus).includes(params.status as AllocationStatus)
? params.status as AllocationStatus
: undefined;
const readModel = await caller.listView({
resourceId: params.resourceId,
projectId: params.projectId,
status,
});
const resourceNameQuery = params.resourceName?.trim().toLowerCase();
const projectCodeQuery = params.projectCode?.trim().toLowerCase();
const limit = Math.min(params.limit ?? 30, 50);
return readModel.assignments
.filter((assignment) => {
if (
resourceNameQuery
&& !assignment.resource?.displayName?.toLowerCase().includes(resourceNameQuery)
) {
return false;
}
if (
projectCodeQuery
&& !assignment.project?.shortCode?.toLowerCase().includes(projectCodeQuery)
) {
return false;
}
return true;
})
.slice(0, limit)
.map((assignment) => ({
id: assignment.id,
resource: assignment.resource?.displayName ?? "Unknown",
resourceEid: assignment.resource?.eid ?? null,
project: assignment.project?.name ?? "Unknown",
projectCode: assignment.project?.shortCode ?? null,
role: assignment.role ?? assignment.roleEntity?.name ?? null,
status: assignment.status,
hoursPerDay: assignment.hoursPerDay,
dailyCost: fmtEur(assignment.dailyCostCents),
start: fmtDate(new Date(assignment.startDate)),
end: fmtDate(new Date(assignment.endDate)),
}));
},
async get_budget_status(params: { projectId: string }, ctx: ToolContext) {
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
const caller = createTimelineCaller(createScopedCallerContext(ctx));
const budgetStatus = await caller.getBudgetStatus({ projectId: project.id });
if (budgetStatus.budgetCents <= 0) {
return {
project: budgetStatus.projectName,
code: budgetStatus.projectCode,
budget: "Not set",
note: "No budget defined for this project",
totalAllocations: budgetStatus.totalAllocations,
};
}
return {
project: budgetStatus.projectName,
code: budgetStatus.projectCode,
budget: fmtEur(budgetStatus.budgetCents),
confirmed: fmtEur(budgetStatus.confirmedCents),
proposed: fmtEur(budgetStatus.proposedCents),
allocated: fmtEur(budgetStatus.allocatedCents),
remaining: fmtEur(budgetStatus.remainingCents),
utilization: `${budgetStatus.utilizationPercent.toFixed(1)}%`,
winWeighted: fmtEur(budgetStatus.winProbabilityWeightedCents),
};
},
async get_vacation_balance(params: { resourceId: string; year?: number }, ctx: ToolContext) {
const year = params.year ?? new Date().getFullYear();
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
return caller.getBalanceDetail({ resourceId: resource.id, year });
},
async list_vacations_upcoming(params: {
resourceName?: string; chapter?: string; daysAhead?: number; limit?: number;
}, ctx: ToolContext) {
const daysAhead = params.daysAhead ?? 30;
const limit = Math.min(params.limit ?? 30, 50);
const caller = createVacationCaller(createScopedCallerContext(ctx));
const now = new Date();
const until = new Date(now.getTime() + daysAhead * 24 * 60 * 60 * 1000);
const vacations = await caller.list({
status: "APPROVED",
startDate: now,
endDate: until,
limit,
});
const filtered = vacations
.filter((vacation) => {
if (params.resourceName) {
const resourceName = vacation.resource?.displayName?.toLowerCase() ?? "";
if (!resourceName.includes(params.resourceName.toLowerCase())) {
return false;
}
}
if (params.chapter) {
const chapter = vacation.resource?.chapter?.toLowerCase() ?? "";
if (!chapter.includes(params.chapter.toLowerCase())) {
return false;
}
}
return true;
})
.slice(0, limit);
return filtered.map((v) => ({
resource: v.resource.displayName,
eid: v.resource.eid,
chapter: v.resource.chapter ?? null,
type: v.type,
start: fmtDate(v.startDate),
end: fmtDate(v.endDate),
isHalfDay: v.isHalfDay,
halfDayPart: v.halfDayPart,
}));
},
async list_holidays_by_region(params: {
countryCode: string;
federalState?: string;
metroCity?: string;
year?: number;
periodStart?: string;
periodEnd?: string;
}, ctx: ToolContext) {
const { year, periodStart, periodEnd } = resolveHolidayPeriod(params);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const resolved = await caller.resolveHolidaysDetail({
periodStart,
periodEnd,
countryCode: params.countryCode.trim().toUpperCase(),
...(params.federalState ? { stateCode: params.federalState } : {}),
...(params.metroCity ? { metroCityName: params.metroCity } : {}),
});
return {
locationContext: resolved.locationContext,
year,
periodStart: resolved.periodStart,
periodEnd: resolved.periodEnd,
count: resolved.count,
summary: resolved.summary,
holidays: resolved.holidays,
};
},
async get_resource_holidays(params: {
identifier: string;
year?: number;
periodStart?: string;
periodEnd?: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.identifier);
if ("error" in resource) return resource;
const { year, periodStart, periodEnd } = resolveHolidayPeriod(params);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const resolved = await caller.resolveResourceHolidaysDetail({
resourceId: resource.id,
periodStart,
periodEnd,
});
return {
resource: resolved.resource,
year,
periodStart: resolved.periodStart,
periodEnd: resolved.periodEnd,
count: resolved.count,
summary: resolved.summary,
holidays: resolved.holidays,
};
},
async list_holiday_calendars(params: {
includeInactive?: boolean;
countryCode?: string;
scopeType?: "COUNTRY" | "STATE" | "CITY";
stateCode?: string;
metroCity?: string;
}, ctx: ToolContext) {
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
return caller.listCalendarsDetail(params);
},
async get_holiday_calendar(params: {
identifier: string;
}, ctx: ToolContext) {
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
return caller.getCalendarByIdentifierDetail({ identifier: params.identifier.trim() });
},
async preview_resolved_holiday_calendar(params: {
countryId: string;
stateCode?: string;
metroCityId?: string;
year: number;
}, ctx: ToolContext) {
const input = PreviewResolvedHolidaysSchema.parse(params);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
return caller.previewResolvedHolidaysDetail(input);
},
async create_holiday_calendar(params: {
name: string;
scopeType: "COUNTRY" | "STATE" | "CITY";
countryId: string;
stateCode?: string;
metroCityId?: string;
isActive?: boolean;
priority?: number;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const created = await caller.createCalendar(CreateHolidayCalendarSchema.parse(params));
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
calendar: formatHolidayCalendar(created),
message: `Created holiday calendar: ${created.name}`,
};
},
async update_holiday_calendar(params: {
id: string;
data: {
name?: string;
stateCode?: string | null;
metroCityId?: string | null;
isActive?: boolean;
priority?: number;
};
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateHolidayCalendarSchema.parse(params.data),
};
const updated = await caller.updateCalendar(input);
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
calendar: formatHolidayCalendar(updated),
message: `Updated holiday calendar: ${updated.name}`,
};
},
async delete_holiday_calendar(params: {
id: string;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const deleted = await caller.deleteCalendar({ id: params.id });
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
message: `Deleted holiday calendar: ${deleted.name}`,
};
},
async create_holiday_calendar_entry(params: {
holidayCalendarId: string;
date: string;
name: string;
isRecurringAnnual?: boolean;
source?: string;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const created = await caller.createEntry(CreateHolidayCalendarEntrySchema.parse(params));
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
entry: formatHolidayCalendarEntry(created),
message: `Created holiday entry: ${created.name}`,
};
},
async update_holiday_calendar_entry(params: {
id: string;
data: {
date?: string;
name?: string;
isRecurringAnnual?: boolean;
source?: string | null;
};
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateHolidayCalendarEntrySchema.parse(params.data),
};
const updated = await caller.updateEntry(input);
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
entry: formatHolidayCalendarEntry(updated),
message: `Updated holiday entry: ${updated.name}`,
};
},
async delete_holiday_calendar_entry(params: {
id: string;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createHolidayCalendarCaller(createScopedCallerContext(ctx));
const deleted = await caller.deleteEntry({ id: params.id });
return {
__action: "invalidate",
scope: ["holidayCalendar", "vacation"],
success: true,
message: `Deleted holiday entry: ${deleted.name}`,
};
},
async list_roles(_params: Record<string, never>, ctx: ToolContext) {
const caller = createRoleCaller(createScopedCallerContext(ctx));
const roles = await caller.list({});
return roles.map((role) => ({
id: role.id,
name: role.name,
color: role.color,
}));
},
async search_by_skill(params: { skill: string }, ctx: ToolContext) {
const caller = createResourceCaller(createScopedCallerContext(ctx));
const matched = await caller.searchBySkills({
rules: [{ skill: params.skill, minProficiency: 1 }],
operator: "OR",
});
return matched.slice(0, 20).map((resource) => ({
id: resource.id,
eid: resource.eid,
name: resource.displayName,
matchedSkill: resource.matchedSkills[0]?.skill ?? null,
level: resource.matchedSkills[0]?.proficiency ?? null,
chapter: resource.chapter ?? null,
}));
},
async get_statistics(_params: Record<string, never>, ctx: ToolContext) {
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getStatisticsDetail();
},
async get_chargeability(params: { resourceId: string; month?: string }, ctx: ToolContext) {
const now = new Date();
const month = params.month ?? `${now.getFullYear()}-${String(now.getMonth() + 1).padStart(2, "0")}`;
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = createResourceCaller(createScopedCallerContext(ctx));
return caller.getChargeabilitySummary({
resourceId: resource.id,
month,
});
},
async get_chargeability_report(params: {
startMonth: string;
endMonth: string;
orgUnitId?: string;
managementLevelGroupId?: string;
countryId?: string;
includeProposed?: boolean;
resourceQuery?: string;
resourceLimit?: number;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const caller = createChargeabilityReportCaller(createScopedCallerContext(ctx));
return caller.getDetail({
startMonth: params.startMonth,
endMonth: params.endMonth,
...(params.orgUnitId ? { orgUnitId: params.orgUnitId } : {}),
...(params.managementLevelGroupId ? { managementLevelGroupId: params.managementLevelGroupId } : {}),
...(params.countryId ? { countryId: params.countryId } : {}),
includeProposed: params.includeProposed ?? false,
...(params.resourceQuery ? { resourceQuery: params.resourceQuery } : {}),
...(params.resourceLimit !== undefined ? { resourceLimit: params.resourceLimit } : {}),
});
},
async get_resource_computation_graph(params: {
resourceId: string;
month: string;
domain?: string;
includeLinks?: boolean;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = createComputationGraphCaller(createScopedCallerContext(ctx));
return caller.getResourceDataDetail({
resourceId: resource.id,
month: params.month,
...(params.domain ? { domain: params.domain } : {}),
...(params.includeLinks !== undefined ? { includeLinks: params.includeLinks } : {}),
});
},
async get_project_computation_graph(params: {
projectId: string;
domain?: string;
includeLinks?: boolean;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
assertPermission(ctx, PermissionKey.USE_ASSISTANT_ADVANCED_TOOLS);
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
const caller = createComputationGraphCaller(createScopedCallerContext(ctx));
return caller.getProjectDataDetail({
projectId: project.id,
...(params.domain ? { domain: params.domain } : {}),
...(params.includeLinks !== undefined ? { includeLinks: params.includeLinks } : {}),
});
},
async search_estimates(params: {
projectCode?: string; query?: string; status?: string; limit?: number;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let projectId: string | undefined;
if (params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
return caller.list({
...(params.query ? { query: params.query } : {}),
...(params.status ? { status: params.status as EstimateStatus } : {}),
...(projectId ? { projectId } : {}),
});
},
async list_clients(params: { query?: string; limit?: number }, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 20, 50);
const caller = createClientCaller(createScopedCallerContext(ctx));
const clients = await caller.list({
isActive: true,
...(params.query ? { search: params.query } : {}),
});
return clients.slice(0, limit).map((c) => ({
id: c.id,
name: c.name,
code: c.code,
projectCount: c._count.projects,
}));
},
async list_org_units(params: { level?: number }, ctx: ToolContext) {
const caller = createOrgUnitCaller(createScopedCallerContext(ctx));
const units = await caller.list({
isActive: true,
...(params.level !== undefined ? { level: params.level } : {}),
});
const details = await Promise.all(units.map((unit) => caller.getById({ id: unit.id })));
return details.map((u) => ({
id: u.id,
name: u.name,
shortName: u.shortName,
level: u.level,
parent: u.parent?.name ?? null,
resourceCount: u._count.resources,
}));
},
// ── NAVIGATION TOOLS ──
async navigate_to_page(params: {
page: string;
eids?: string; chapters?: string; projectIds?: string; clientIds?: string;
countryCodes?: string; startDate?: string; days?: number;
}, _ctx: ToolContext) {
const pageMap: Record<string, string> = {
timeline: "/timeline",
dashboard: "/dashboard",
resources: "/resources",
projects: "/projects",
allocations: "/allocations",
staffing: "/staffing",
estimates: "/estimates",
vacations: "/vacations",
"my-vacations": "/vacations/my",
roles: "/roles",
"skills-analytics": "/analytics/skills",
chargeability: "/reports/chargeability",
"computation-graph": "/analytics/computation-graph",
};
const path = pageMap[params.page];
if (!path) return { error: `Unknown page: ${params.page}. Available: ${Object.keys(pageMap).join(", ")}` };
// Build query params for pages that support them
const queryParts: string[] = [];
if (params.eids) queryParts.push(`eids=${encodeURIComponent(params.eids)}`);
if (params.chapters) queryParts.push(`chapters=${encodeURIComponent(params.chapters)}`);
if (params.projectIds) queryParts.push(`projectIds=${encodeURIComponent(params.projectIds)}`);
if (params.clientIds) queryParts.push(`clientIds=${encodeURIComponent(params.clientIds)}`);
if (params.countryCodes) queryParts.push(`countryCodes=${encodeURIComponent(params.countryCodes)}`);
if (params.startDate) queryParts.push(`startDate=${encodeURIComponent(params.startDate)}`);
if (params.days) queryParts.push(`days=${params.days}`);
const url = queryParts.length > 0 ? `${path}?${queryParts.join("&")}` : path;
return {
__action: "navigate",
url,
description: `Navigiere zu ${path}`,
};
},
// ── WRITE TOOLS ──
async create_allocation(params: {
resourceId: string; projectId: string;
startDate: string; endDate: string;
hoursPerDay: number; role?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, params.resourceId),
resolveProjectIdentifier(ctx, params.projectId),
]);
if ("error" in resource) {
return resource;
}
if ("error" in project) {
return project;
}
const caller = createAllocationCaller(createScopedCallerContext(ctx));
try {
const result = await caller.ensureAssignment({
resourceId: resource.id,
projectId: project.id,
startDate: new Date(`${params.startDate}T00:00:00.000Z`),
endDate: new Date(`${params.endDate}T00:00:00.000Z`),
hoursPerDay: params.hoursPerDay,
...(params.role ? { role: params.role } : {}),
});
return {
__action: "invalidate",
scope: ["allocation", "timeline"],
success: true,
message: `${result.action === "reactivated" ? "Reactivated" : "Created"} allocation: ${resource.displayName}${project.name} (${project.shortCode}), ${params.hoursPerDay}h/day, ${params.startDate} to ${params.endDate}`,
allocationId: result.assignment.id,
status: result.assignment.status,
};
} catch (error) {
if (error instanceof TRPCError && error.code === "CONFLICT") {
return { error: "Allocation already exists for this resource/project/dates. No new allocation created." };
}
throw error;
}
},
async cancel_allocation(params: {
allocationId?: string;
resourceName?: string; projectCode?: string;
startDate?: string; endDate?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const caller = createAllocationCaller(createScopedCallerContext(ctx));
let resourceId: string | undefined;
let projectId: string | undefined;
if (!params.allocationId && params.resourceName && params.projectCode) {
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, params.resourceName),
resolveProjectIdentifier(ctx, params.projectCode),
]);
if ("error" in resource) {
return resource;
}
if ("error" in project) {
return project;
}
resourceId = resource.id;
projectId = project.id;
}
let assignment;
try {
assignment = await caller.resolveAssignment({
...(params.allocationId ? { assignmentId: params.allocationId } : {}),
...(resourceId ? { resourceId } : {}),
...(projectId ? { projectId } : {}),
...(params.startDate ? { startDate: new Date(`${params.startDate}T00:00:00.000Z`) } : {}),
...(params.endDate ? { endDate: new Date(`${params.endDate}T00:00:00.000Z`) } : {}),
selectionMode: "WINDOW",
excludeCancelled: true,
});
} catch {
return { error: "Allocation not found with the given criteria." };
}
await caller.updateAssignment({
id: assignment.id,
data: UpdateAssignmentSchema.parse({ status: AllocationStatus.CANCELLED }),
});
return {
__action: "invalidate",
scope: ["allocation", "timeline"],
success: true,
message: `Cancelled allocation: ${assignment.resource.displayName}${assignment.project.name} (${assignment.project.shortCode}), ${fmtDate(assignment.startDate)} to ${fmtDate(assignment.endDate)}`,
};
},
async update_allocation_status(params: {
allocationId?: string;
resourceName?: string; projectCode?: string;
startDate?: string;
newStatus: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const validStatuses = ["PROPOSED", "CONFIRMED", "ACTIVE", "COMPLETED", "CANCELLED"];
if (!validStatuses.includes(params.newStatus)) {
return { error: `Invalid status: ${params.newStatus}. Valid: ${validStatuses.join(", ")}` };
}
const caller = createAllocationCaller(createScopedCallerContext(ctx));
let resourceId: string | undefined;
let projectId: string | undefined;
if (!params.allocationId && params.resourceName && params.projectCode) {
const [resource, project] = await Promise.all([
resolveResourceIdentifier(ctx, params.resourceName),
resolveProjectIdentifier(ctx, params.projectCode),
]);
if ("error" in resource) {
return resource;
}
if ("error" in project) {
return project;
}
resourceId = resource.id;
projectId = project.id;
}
let assignment;
try {
assignment = await caller.resolveAssignment({
...(params.allocationId ? { assignmentId: params.allocationId } : {}),
...(resourceId ? { resourceId } : {}),
...(projectId ? { projectId } : {}),
...(params.startDate ? { startDate: new Date(`${params.startDate}T00:00:00.000Z`) } : {}),
selectionMode: "EXACT_START",
});
} catch {
return { error: "Allocation not found with the given criteria." };
}
const oldStatus = assignment.status;
await caller.updateAssignment({
id: assignment.id,
data: UpdateAssignmentSchema.parse({
status: params.newStatus as AllocationStatus,
}),
});
return {
__action: "invalidate",
scope: ["allocation", "timeline"],
success: true,
message: `Updated allocation status: ${assignment.resource.displayName}${assignment.project.name} (${assignment.project.shortCode}), ${fmtDate(assignment.startDate)} to ${fmtDate(assignment.endDate)}: ${oldStatus}${params.newStatus}`,
};
},
async update_resource(params: {
id: string; displayName?: string; fte?: number;
lcrCents?: number; chapter?: string; chargeabilityTarget?: number;
}, ctx: ToolContext) {
assertPermission(ctx, "manageResources" as PermissionKey);
const resource = await resolveResourceIdentifier(ctx, params.id);
if ("error" in resource) return resource;
const caller = createResourceCaller(createScopedCallerContext(ctx));
const data = UpdateResourceSchema.parse({
...(params.displayName !== undefined ? { displayName: params.displayName } : {}),
...(params.fte !== undefined ? { fte: params.fte } : {}),
...(params.lcrCents !== undefined ? { lcrCents: params.lcrCents } : {}),
...(params.chapter !== undefined ? { chapter: params.chapter } : {}),
...(params.chargeabilityTarget !== undefined ? { chargeabilityTarget: params.chargeabilityTarget } : {}),
});
const updatedFields = Object.keys(data);
if (updatedFields.length === 0) return { error: "No fields to update" };
const updated = await caller.update({ id: resource.id, data });
return {
__action: "invalidate",
scope: ["resource"],
success: true,
message: `Updated resource ${updated.displayName} (${updated.eid})`,
updatedFields,
};
},
async update_project(params: {
id: string; name?: string; budgetCents?: number;
winProbability?: number; status?: string;
responsiblePerson?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
const project = await resolveProjectIdentifier(ctx, params.id);
if ("error" in project) return project;
const data: Record<string, unknown> = {};
if (params.name !== undefined) data.name = params.name;
if (params.budgetCents !== undefined) data.budgetCents = params.budgetCents;
if (params.winProbability !== undefined) data.winProbability = params.winProbability;
if (params.status !== undefined) data.status = params.status;
// Validate responsible person against existing resources
if (params.responsiblePerson !== undefined) {
const result = await resolveResponsiblePerson(params.responsiblePerson, ctx);
if ("error" in result) return { error: result.error };
data.responsiblePerson = result.displayName;
}
const parsedData = UpdateProjectSchema.parse(data);
const updatedFields = Object.keys(parsedData);
if (updatedFields.length === 0) return { error: "No fields to update" };
const caller = createProjectCaller(createScopedCallerContext(ctx));
const updated = await caller.update({ id: project.id, data: parsedData });
return {
__action: "invalidate",
scope: ["project"],
success: true,
message: `Updated project ${updated.name} (${updated.shortCode})`,
updatedFields,
};
},
async create_project(params: {
shortCode: string; name: string; orderType: string;
allocationType?: string; budgetCents: number;
startDate: string; endDate: string;
winProbability?: number; status?: string;
responsiblePerson?: string; color?: string;
blueprintName?: string; clientName?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
if (!params.responsiblePerson?.trim()) {
return { error: "responsiblePerson is required to create a project." };
}
// Validate responsible person against existing resources
const responsible = await resolveResponsiblePerson(params.responsiblePerson, ctx);
if ("error" in responsible) return { error: responsible.error };
const blueprintCaller = createBlueprintCaller(createScopedCallerContext(ctx));
const clientCaller = createClientCaller(createScopedCallerContext(ctx));
let blueprintId: string | undefined;
if (params.blueprintName) {
const blueprint = await resolveEntityOrAssistantError(
() => blueprintCaller.resolveByIdentifier({ identifier: params.blueprintName! }),
`Blueprint not found: "${params.blueprintName}"`,
);
if ("error" in blueprint) {
return blueprint;
}
blueprintId = blueprint.id;
}
let clientId: string | undefined;
if (params.clientName) {
const client = await resolveEntityOrAssistantError(
() => clientCaller.resolveByIdentifier({ identifier: params.clientName! }),
`Client not found: "${params.clientName}"`,
);
if ("error" in client) {
return client;
}
clientId = client.id;
}
const input = CreateProjectSchema.parse({
shortCode: params.shortCode,
name: params.name,
orderType: params.orderType,
allocationType: params.allocationType ?? "INT",
budgetCents: params.budgetCents,
startDate: params.startDate,
endDate: params.endDate,
winProbability: params.winProbability ?? 100,
status: params.status ?? "DRAFT",
responsiblePerson: responsible.displayName,
...(params.color ? { color: params.color } : {}),
...(blueprintId ? { blueprintId } : {}),
...(clientId ? { clientId } : {}),
staffingReqs: [],
dynamicFields: {},
});
const caller = createProjectCaller(createScopedCallerContext(ctx));
const project = await caller.create(input);
return {
__action: "invalidate",
scope: ["project"],
success: true,
message: `Created project: ${project.name} (${project.shortCode}), budget ${fmtEur(params.budgetCents)}, ${params.startDate} to ${params.endDate}, status: ${project.status}`,
projectId: project.id,
shortCode: project.shortCode,
};
},
// ── RESOURCE MANAGEMENT ──
async create_resource(params: {
eid: string; displayName: string; email?: string;
fte?: number; lcrCents: number; ucrCents?: number;
chapter?: string; chargeabilityTarget?: number;
roleName?: string; countryCode?: string; orgUnitName?: string;
postalCode?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageResources" as PermissionKey);
if (!params.email?.trim()) {
return { error: "email is required to create a resource." };
}
const roleCaller = createRoleCaller(createScopedCallerContext(ctx));
const countryCaller = createCountryCaller(createScopedCallerContext(ctx));
const orgUnitCaller = createOrgUnitCaller(createScopedCallerContext(ctx));
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const data: Record<string, any> = {
eid: params.eid,
displayName: params.displayName,
email: params.email,
lcrCents: params.lcrCents,
ucrCents: params.ucrCents ?? Math.round(params.lcrCents * 0.7),
chargeabilityTarget: params.chargeabilityTarget ?? 80,
currency: "EUR",
availability: {
monday: 8,
tuesday: 8,
wednesday: 8,
thursday: 8,
friday: 8,
},
skills: [],
dynamicFields: {},
...(params.fte !== undefined ? { fte: params.fte } : {}),
};
if (params.chapter) data.chapter = params.chapter;
if (params.postalCode) data.postalCode = params.postalCode;
if (params.roleName) {
const role = await resolveEntityOrAssistantError(
() => roleCaller.resolveByIdentifier({ identifier: params.roleName! }),
`Role not found: "${params.roleName}"`,
);
if ("error" in role) {
return role;
}
data.roleId = role.id;
}
if (params.countryCode) {
const country = await resolveEntityOrAssistantError(
() => countryCaller.resolveByIdentifier({ identifier: params.countryCode! }),
`Country not found: "${params.countryCode}"`,
);
if ("error" in country) {
return country;
}
data.countryId = country.id;
}
if (params.orgUnitName) {
const orgUnit = await resolveEntityOrAssistantError(
() => orgUnitCaller.resolveByIdentifier({ identifier: params.orgUnitName! }),
`Org unit not found: "${params.orgUnitName}"`,
);
if ("error" in orgUnit) {
return orgUnit;
}
data.orgUnitId = orgUnit.id;
}
const input = CreateResourceSchema.parse(data);
const caller = createResourceCaller(createScopedCallerContext(ctx));
const resource = await caller.create(input);
return {
__action: "invalidate",
scope: ["resource"],
success: true,
message: `Created resource: ${resource.displayName} (${resource.eid})`,
resourceId: resource.id,
};
},
async deactivate_resource(params: { identifier: string }, ctx: ToolContext) {
assertPermission(ctx, "manageResources" as PermissionKey);
const resource = await resolveResourceIdentifier(ctx, params.identifier);
if ("error" in resource) return resource;
const caller = createResourceCaller(createScopedCallerContext(ctx));
await caller.deactivate({ id: resource.id });
if (!resource) return { error: `Resource not found: ${params.identifier}` };
return {
__action: "invalidate",
scope: ["resource"],
success: true,
message: `Deactivated resource: ${resource.displayName} (${resource.eid})`,
};
},
// ── VACATION MANAGEMENT ──
async create_vacation(params: {
resourceId: string; type: string;
startDate: string; endDate: string;
isHalfDay?: boolean; halfDayPart?: string; note?: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
const caller = createVacationCaller(createScopedCallerContext(ctx));
const type = parseAssistantVacationRequestType(params.type);
const vacation = await caller.create({
resourceId: resource.id,
type,
startDate: new Date(params.startDate),
endDate: new Date(params.endDate),
...(params.isHalfDay !== undefined ? { isHalfDay: params.isHalfDay } : {}),
...(params.halfDayPart !== undefined ? { halfDayPart: params.halfDayPart as "MORNING" | "AFTERNOON" } : {}),
...(params.note !== undefined ? { note: params.note } : {}),
});
const effectiveDays = "effectiveDays" in vacation && typeof vacation.effectiveDays === "number"
? vacation.effectiveDays
: null;
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
message: `Created ${type} for ${resource.displayName}: ${params.startDate} to ${params.endDate} (status: ${vacation.status}${effectiveDays !== null ? `, deducted ${effectiveDays} day(s)` : ""})`,
vacationId: vacation.id,
vacation,
};
},
async approve_vacation(params: { vacationId: string }, ctx: ToolContext) {
const caller = createVacationCaller(createScopedCallerContext(ctx));
const existing = await caller.getById({ id: params.vacationId });
const approved = await caller.approve({ id: params.vacationId });
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
warnings: approved.warnings,
vacation: approved,
message: `Approved vacation for ${existing.resource?.displayName ?? params.vacationId}`,
};
},
async reject_vacation(params: { vacationId: string; reason?: string }, ctx: ToolContext) {
const caller = createVacationCaller(createScopedCallerContext(ctx));
const existing = await caller.getById({ id: params.vacationId });
const rejected = await caller.reject({
id: params.vacationId,
...(params.reason !== undefined ? { rejectionReason: params.reason } : {}),
});
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
vacation: rejected,
message: `Rejected vacation for ${existing.resource?.displayName ?? params.vacationId}${params.reason ? `: ${params.reason}` : ""}`,
};
},
async cancel_vacation(params: { vacationId: string }, ctx: ToolContext) {
const caller = createVacationCaller(createScopedCallerContext(ctx));
const existing = await caller.getById({ id: params.vacationId });
const cancelled = await caller.cancel({ id: params.vacationId });
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
vacation: cancelled,
message: `Cancelled vacation for ${existing.resource?.displayName ?? params.vacationId}`,
};
},
async get_pending_vacation_approvals(params: { limit?: number }, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 20, 50);
const caller = createVacationCaller(createScopedCallerContext(ctx));
const vacations = await caller.getPendingApprovals();
return vacations.map((v) => ({
id: v.id,
resource: v.resource.displayName,
eid: v.resource.eid,
chapter: v.resource.chapter,
type: v.type,
start: fmtDate(v.startDate),
end: fmtDate(v.endDate),
isHalfDay: v.isHalfDay,
})).slice(0, limit);
},
async get_team_vacation_overlap(params: {
resourceId: string; startDate: string; endDate: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = createVacationCaller(createScopedCallerContext(ctx));
return caller.getTeamOverlapDetail({
resourceId: resource.id,
startDate: new Date(params.startDate),
endDate: new Date(params.endDate),
});
},
// ── ENTITLEMENT ──
async get_entitlement_summary(params: { year?: number; resourceName?: string }, ctx: ToolContext) {
const year = params.year ?? new Date().getFullYear();
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
return caller.getYearSummaryDetail({
year,
...(params.resourceName ? { resourceName: params.resourceName } : {}),
});
},
async set_entitlement(params: {
resourceId: string; year: number; entitledDays: number; carryoverDays?: number;
}, ctx: ToolContext) {
if (params.carryoverDays !== undefined) {
return {
error: "Manual carryoverDays is not supported here. Carryover is computed automatically from prior-year balances.",
};
}
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
const caller = createEntitlementCaller(createScopedCallerContext(ctx));
const entitlement = await caller.set({
resourceId: resource.id,
year: params.year,
entitledDays: params.entitledDays,
});
return {
__action: "invalidate",
scope: ["vacation"],
success: true,
entitlement,
message: `Set entitlement for ${resource.displayName} (${params.year}): ${params.entitledDays} days`,
};
},
// ── DEMAND / STAFFING ──
async list_demands(params: { projectId?: string; status?: string; limit?: number }, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 30, 50);
const caller = createAllocationCaller(createScopedCallerContext(ctx));
const resolvedProject = params.projectId
? await resolveProjectIdentifier(ctx, params.projectId)
: null;
if (resolvedProject && "error" in resolvedProject) {
return resolvedProject;
}
const demands = await caller.listDemands({
...(resolvedProject ? { projectId: resolvedProject.id } : {}),
...(params.status ? { status: params.status as AllocationStatus } : {}),
});
return demands.map((d) => ({
id: d.id,
project: d.project.name,
projectCode: d.project.shortCode,
role: d.roleEntity?.name ?? d.role ?? "Unspecified",
status: d.status,
headcount: d.headcount,
filled: d.assignments.length,
remaining: d.headcount - d.assignments.length,
hoursPerDay: d.hoursPerDay,
start: fmtDate(d.startDate),
end: fmtDate(d.endDate),
})).slice(0, limit);
},
async create_demand(params: {
projectId: string; roleName: string; headcount?: number;
hoursPerDay: number; startDate: string; endDate: string;
}, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const roleCaller = createRoleCaller(createScopedCallerContext(ctx));
const [project, role] = await Promise.all([
resolveProjectIdentifier(ctx, params.projectId),
resolveEntityOrAssistantError(
() => roleCaller.resolveByIdentifier({ identifier: params.roleName }),
`Role not found: ${params.roleName}`,
),
]);
if ("error" in project) {
return project;
}
if ("error" in role) {
return role;
}
const caller = createAllocationCaller(createScopedCallerContext(ctx));
const demand = await caller.createDemand({
projectId: project.id,
roleId: role.id,
role: role.name,
headcount: params.headcount ?? 1,
hoursPerDay: params.hoursPerDay,
startDate: parseIsoDate(params.startDate, "startDate"),
endDate: parseIsoDate(params.endDate, "endDate"),
});
return {
__action: "invalidate",
scope: ["allocation"],
success: true,
message: `Created demand: ${role.name} × ${params.headcount ?? 1} for ${project.name} (${project.shortCode}), ${params.hoursPerDay}h/day, ${params.startDate} to ${params.endDate}`,
demandId: demand.id,
};
},
async fill_demand(params: { demandId: string; resourceId: string }, ctx: ToolContext) {
assertPermission(ctx, "manageAllocations" as PermissionKey);
const allocationCaller = createAllocationCaller(createScopedCallerContext(ctx));
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) return resource;
const result = await allocationCaller.assignResourceToDemand({
demandRequirementId: params.demandId,
resourceId: resource.id,
});
const roleName = result.demandRequirement.roleEntity?.name ?? result.demandRequirement.role ?? null;
return {
__action: "invalidate",
scope: ["allocation", "timeline"],
success: true,
message: `Assigned ${resource.displayName} to ${roleName ?? "demand"} on ${result.demandRequirement.project.name} (${result.demandRequirement.project.shortCode})`,
assignmentId: result.assignment.id,
};
},
async check_resource_availability(params: {
resourceId: string; startDate: string; endDate: string;
}, ctx: ToolContext) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
const caller = createAllocationCaller(createScopedCallerContext(ctx));
return caller.getResourceAvailabilitySummary({
resourceId: resource.id,
startDate: new Date(`${params.startDate}T00:00:00.000Z`),
endDate: new Date(`${params.endDate}T00:00:00.000Z`),
});
},
async get_staffing_suggestions(params: {
projectId: string; roleName?: string;
startDate?: string; endDate?: string; limit?: number;
}, ctx: ToolContext) {
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
const caller = createStaffingCaller(createScopedCallerContext(ctx));
return caller.getProjectStaffingSuggestions({
projectId: project.id,
...(params.roleName ? { roleName: params.roleName } : {}),
...(params.startDate ? { startDate: new Date(params.startDate) } : {}),
...(params.endDate ? { endDate: new Date(params.endDate) } : {}),
...(params.limit ? { limit: params.limit } : {}),
});
},
async find_capacity(params: {
startDate: string; endDate: string;
minHoursPerDay?: number; roleName?: string;
chapter?: string; limit?: number;
}, ctx: ToolContext) {
const caller = createStaffingCaller(createScopedCallerContext(ctx));
return caller.searchCapacity({
startDate: new Date(params.startDate),
endDate: new Date(params.endDate),
minHoursPerDay: params.minHoursPerDay ?? 4,
...(params.roleName ? { roleName: params.roleName } : {}),
...(params.chapter ? { chapter: params.chapter } : {}),
...(params.limit ? { limit: params.limit } : {}),
});
},
// ── BLUEPRINT ──
async list_blueprints(_params: Record<string, never>, ctx: ToolContext) {
const caller = createBlueprintCaller(createScopedCallerContext(ctx));
const blueprints = await caller.listSummaries();
return blueprints.map((b) => ({
id: b.id,
name: b.name,
projectCount: b._count.projects,
}));
},
async get_blueprint(params: { identifier: string }, ctx: ToolContext) {
const caller = createBlueprintCaller(createScopedCallerContext(ctx));
const bp = await caller.getByIdentifier({ identifier: params.identifier });
return {
id: bp.id,
name: bp.name,
fieldDefs: bp.fieldDefs,
rolePresets: bp.rolePresets,
};
},
// ── RATE CARDS ──
async list_rate_cards(params: { query?: string; limit?: number }, ctx: ToolContext) {
const caller = createRateCardCaller(createScopedCallerContext(ctx));
const cards = await caller.list({
isActive: true,
...(params.query ? { search: params.query } : {}),
});
return cards.map((c) => ({
id: c.id,
name: c.name,
effectiveFrom: fmtDate(c.effectiveFrom),
effectiveTo: fmtDate(c.effectiveTo),
lineCount: c._count.lines,
})).slice(0, Math.min(params.limit ?? 20, 50));
},
async resolve_rate(params: { resourceId?: string; roleName?: string; date?: string }, ctx: ToolContext) {
const caller = createRateCardCaller(createScopedCallerContext(ctx));
if (params.resourceId) {
const resource = await resolveResourceIdentifier(ctx, params.resourceId);
if ("error" in resource) {
return resource;
}
return caller.resolveBestRate({
resourceId: resource.id,
...(params.date ? { date: new Date(params.date) } : {}),
});
}
return caller.resolveBestRate({
...(params.roleName ? { roleName: params.roleName } : {}),
...(params.date ? { date: new Date(params.date) } : {}),
});
},
// ── ESTIMATES ──
async get_estimate_detail(params: { estimateId: string }, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
const caller = createEstimateCaller(createScopedCallerContext(ctx));
return caller.getById({ id: params.estimateId });
},
async list_estimate_versions(params: { estimateId: string }, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
return caller.listVersions({ estimateId: params.estimateId });
},
async get_estimate_version_snapshot(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.VIEW_COSTS);
const caller = createEstimateCaller(createScopedCallerContext(ctx));
return caller.getVersionSnapshot({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
},
async create_estimate(params: {
projectId?: string;
projectCode?: string;
name: string;
opportunityId?: string;
baseCurrency?: string;
status?: EstimateStatus;
versionLabel?: string;
versionNotes?: string;
assumptions?: CreateEstimateInput["assumptions"];
scopeItems?: CreateEstimateInput["scopeItems"];
demandLines?: CreateEstimateInput["demandLines"];
resourceSnapshots?: CreateEstimateInput["resourceSnapshots"];
metrics?: CreateEstimateInput["metrics"];
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let projectId = params.projectId;
if (!projectId && params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
const estimate = await caller.create({
name: params.name,
...(projectId ? { projectId } : {}),
...(params.opportunityId !== undefined ? { opportunityId: params.opportunityId } : {}),
...(params.baseCurrency !== undefined ? { baseCurrency: params.baseCurrency } : {}),
...(params.status !== undefined ? { status: params.status } : {}),
...(params.versionLabel !== undefined ? { versionLabel: params.versionLabel } : {}),
...(params.versionNotes !== undefined ? { versionNotes: params.versionNotes } : {}),
...(params.assumptions !== undefined ? { assumptions: params.assumptions } : {}),
...(params.scopeItems !== undefined ? { scopeItems: params.scopeItems } : {}),
...(params.demandLines !== undefined ? { demandLines: params.demandLines } : {}),
...(params.resourceSnapshots !== undefined ? { resourceSnapshots: params.resourceSnapshots } : {}),
...(params.metrics !== undefined ? { metrics: params.metrics } : {}),
});
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Created estimate "${estimate.name}".`,
};
},
async clone_estimate(params: {
sourceEstimateId: string;
name?: string;
projectId?: string;
projectCode?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let projectId = params.projectId;
if (!projectId && params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
const estimate = await caller.clone({
sourceEstimateId: params.sourceEstimateId,
...(params.name !== undefined ? { name: params.name } : {}),
...(projectId ? { projectId } : {}),
});
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Cloned estimate "${estimate.name}".`,
};
},
async update_estimate_draft(params: {
id: string;
projectId?: string;
projectCode?: string;
name?: string;
opportunityId?: string;
baseCurrency?: string;
status?: EstimateStatus;
versionLabel?: string;
versionNotes?: string;
assumptions?: UpdateEstimateDraftInput["assumptions"];
scopeItems?: UpdateEstimateDraftInput["scopeItems"];
demandLines?: UpdateEstimateDraftInput["demandLines"];
resourceSnapshots?: UpdateEstimateDraftInput["resourceSnapshots"];
metrics?: UpdateEstimateDraftInput["metrics"];
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
let projectId = params.projectId;
if (!projectId && params.projectCode) {
const project = await resolveProjectIdentifier(ctx, params.projectCode);
if ("error" in project) {
return project;
}
projectId = project.id;
}
const estimate = await caller.updateDraft({
id: params.id,
...(projectId ? { projectId } : {}),
...(params.name !== undefined ? { name: params.name } : {}),
...(params.opportunityId !== undefined ? { opportunityId: params.opportunityId } : {}),
...(params.baseCurrency !== undefined ? { baseCurrency: params.baseCurrency } : {}),
...(params.status !== undefined ? { status: params.status } : {}),
...(params.versionLabel !== undefined ? { versionLabel: params.versionLabel } : {}),
...(params.versionNotes !== undefined ? { versionNotes: params.versionNotes } : {}),
...(params.assumptions !== undefined ? { assumptions: params.assumptions } : {}),
...(params.scopeItems !== undefined ? { scopeItems: params.scopeItems } : {}),
...(params.demandLines !== undefined ? { demandLines: params.demandLines } : {}),
...(params.resourceSnapshots !== undefined ? { resourceSnapshots: params.resourceSnapshots } : {}),
...(params.metrics !== undefined ? { metrics: params.metrics } : {}),
});
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Updated estimate draft "${estimate.name}".`,
};
},
async submit_estimate_version(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
const estimate = await caller.submitVersion({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Submitted estimate version for "${estimate.name}".`,
};
},
async approve_estimate_version(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
const estimate = await caller.approveVersion({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Approved estimate version for "${estimate.name}".`,
};
},
async create_estimate_revision(params: {
estimateId: string;
sourceVersionId?: string;
label?: string;
notes?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
const estimate = await caller.createRevision({
estimateId: params.estimateId,
...(params.sourceVersionId !== undefined ? { sourceVersionId: params.sourceVersionId } : {}),
...(params.label !== undefined ? { label: params.label } : {}),
...(params.notes !== undefined ? { notes: params.notes } : {}),
});
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Created a new estimate revision for "${estimate.name}".`,
};
},
async create_estimate_export(params: {
estimateId: string;
versionId?: string;
format: EstimateExportFormat;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
const estimate = await caller.createExport({
estimateId: params.estimateId,
format: params.format,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
estimate,
estimateId: estimate.id,
message: `Created ${params.format} export for estimate "${estimate.name}".`,
};
},
async create_estimate_planning_handoff(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
const result = await caller.createPlanningHandoff({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
return {
__action: "invalidate",
scope: ["estimate", "allocation", "timeline"],
success: true,
...result,
message: `Created planning handoff for estimate ${params.estimateId}.`,
};
},
async generate_estimate_weekly_phasing(params: {
estimateId: string;
startDate: string;
endDate: string;
pattern?: "even" | "front_loaded" | "back_loaded" | "custom";
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
const result = await caller.generateWeeklyPhasing({
estimateId: params.estimateId,
startDate: params.startDate,
endDate: params.endDate,
...(params.pattern !== undefined ? { pattern: params.pattern } : {}),
});
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
...result,
message: `Generated weekly phasing for estimate ${params.estimateId}.`,
};
},
async get_estimate_weekly_phasing(params: {
estimateId: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
return caller.getWeeklyPhasing({ estimateId: params.estimateId });
},
async get_estimate_commercial_terms(params: {
estimateId: string;
versionId?: string;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
return caller.getCommercialTerms({
estimateId: params.estimateId,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
},
async update_estimate_commercial_terms(params: {
estimateId: string;
versionId?: string;
terms: Record<string, unknown>;
}, ctx: ToolContext) {
const caller = createEstimateCaller(createScopedCallerContext(ctx));
const result = await caller.updateCommercialTerms({
estimateId: params.estimateId,
terms: params.terms,
...(params.versionId !== undefined ? { versionId: params.versionId } : {}),
});
return {
__action: "invalidate",
scope: ["estimate"],
success: true,
...result,
message: `Updated commercial terms for estimate ${params.estimateId}.`,
};
},
// ── ROLES ──
async create_role(params: {
name: string;
description?: string;
color?: string;
}, ctx: ToolContext) {
const caller = createRoleCaller(createScopedCallerContext(ctx));
const role = await caller.create(CreateRoleSchema.parse(params));
return { __action: "invalidate", scope: ["role"], success: true, message: `Created role: ${role.name}`, roleId: role.id, role };
},
async update_role(params: {
id: string;
name?: string;
description?: string;
color?: string;
isActive?: boolean;
}, ctx: ToolContext) {
const caller = createRoleCaller(createScopedCallerContext(ctx));
const data = UpdateRoleSchema.parse({
...(params.name !== undefined ? { name: params.name } : {}),
...(params.description !== undefined ? { description: params.description } : {}),
...(params.color !== undefined ? { color: params.color } : {}),
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
});
if (Object.keys(data).length === 0) return { error: "No fields to update" };
const role = await caller.update({ id: params.id, data });
return { __action: "invalidate", scope: ["role"], success: true, message: `Updated role: ${role.name}`, roleId: role.id, role };
},
async delete_role(params: { id: string }, ctx: ToolContext) {
const caller = createRoleCaller(createScopedCallerContext(ctx));
const role = await caller.getById({ id: params.id });
await caller.delete({ id: params.id });
return { __action: "invalidate", scope: ["role"], success: true, message: `Deleted role: ${role.name}` };
},
// ── CLIENTS ──
async create_client(params: {
name: string;
code?: string;
parentId?: string;
sortOrder?: number;
tags?: string[];
}, ctx: ToolContext) {
const caller = createClientCaller(createScopedCallerContext(ctx));
const client = await caller.create(CreateClientSchema.parse(params));
return { __action: "invalidate", scope: ["client"], success: true, message: `Created client: ${client.name}`, clientId: client.id, client };
},
async update_client(params: {
id: string;
name?: string;
code?: string | null;
sortOrder?: number;
isActive?: boolean;
parentId?: string | null;
tags?: string[];
}, ctx: ToolContext) {
const caller = createClientCaller(createScopedCallerContext(ctx));
const data = UpdateClientSchema.parse({
...(params.name !== undefined ? { name: params.name } : {}),
...(params.code !== undefined ? { code: params.code } : {}),
...(params.sortOrder !== undefined ? { sortOrder: params.sortOrder } : {}),
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
...(params.parentId !== undefined ? { parentId: params.parentId } : {}),
...(params.tags !== undefined ? { tags: params.tags } : {}),
});
if (Object.keys(data).length === 0) return { error: "No fields to update" };
const client = await caller.update({ id: params.id, data });
return { __action: "invalidate", scope: ["client"], success: true, message: `Updated client: ${client.name}`, clientId: client.id, client };
},
// ── ADMIN / CONFIG ──
async list_countries(params: { includeInactive?: boolean; search?: string }, ctx: ToolContext) {
const caller = createCountryCaller(createScopedCallerContext(ctx));
const countries = await caller.list(
params.includeInactive
? undefined
: { isActive: true },
);
const normalizedSearch = params.search?.trim().toLowerCase();
const filteredCountries = normalizedSearch
? countries.filter((country) =>
country.code.toLowerCase().includes(normalizedSearch)
|| country.name.toLowerCase().includes(normalizedSearch))
: countries;
return {
count: filteredCountries.length,
countries: filteredCountries.map(formatCountry),
};
},
async get_country(params: { identifier: string }, ctx: ToolContext) {
const caller = createCountryCaller(createScopedCallerContext(ctx));
const country = await caller.getByIdentifier({ identifier: params.identifier });
return formatCountry(country);
},
async create_country(params: {
code: string;
name: string;
dailyWorkingHours?: number;
scheduleRules?: Prisma.JsonValue | null;
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
const created = await caller.create(CreateCountrySchema.parse(params));
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
country: formatCountry(created),
message: `Created country: ${created.name}`,
};
},
async update_country(params: {
id: string;
data: {
code?: string;
name?: string;
dailyWorkingHours?: number;
scheduleRules?: Prisma.JsonValue | null;
isActive?: boolean;
};
}, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateCountrySchema.parse(params.data),
};
const updated = await caller.update(input);
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
country: formatCountry(updated),
message: `Updated country: ${updated.name}`,
};
},
async create_metro_city(params: { countryId: string; name: string }, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
const created = await caller.createCity(CreateMetroCitySchema.parse(params));
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
metroCity: created,
message: `Created metro city: ${created.name}`,
};
},
async update_metro_city(params: { id: string; data: { name?: string } }, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
const input = {
id: params.id,
data: UpdateMetroCitySchema.parse(params.data),
};
const updated = await caller.updateCity(input);
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
metroCity: updated,
message: `Updated metro city: ${updated.name}`,
};
},
async delete_metro_city(params: { id: string }, ctx: ToolContext) {
assertAdminRole(ctx);
const caller = createCountryCaller(createScopedCallerContext(ctx));
const deleted = await caller.deleteCity({ id: params.id });
return {
__action: "invalidate",
scope: ["country", "resource", "holidayCalendar", "vacation"],
success: true,
message: `Deleted metro city: ${deleted.name ?? params.id}`,
};
},
async list_management_levels(_params: Record<string, never>, ctx: ToolContext) {
const caller = createManagementLevelCaller(createScopedCallerContext(ctx));
const groups = await caller.listGroups();
return groups.map((g) => ({
id: g.id,
name: g.name,
target: g.targetPercentage ? `${g.targetPercentage}%` : null,
levels: g.levels.map((l: { id: string; name: string }) => ({ id: l.id, name: l.name })),
}));
},
async list_utilization_categories(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUtilizationCategoryCaller(createScopedCallerContext(ctx));
const categories = await caller.list();
const categoriesWithCounts = await Promise.all(
categories.map(async (category) => {
const detailed = await caller.getById({ id: category.id });
return {
category,
projectCount: detailed._count.projects,
};
}),
);
return categoriesWithCounts.map(({ category, projectCount }) => ({
id: category.id,
code: category.code,
name: category.name,
description: category.description,
projectCount,
}));
},
async list_calculation_rules(_params: Record<string, never>, ctx: ToolContext) {
const caller = createCalculationRuleCaller(createScopedCallerContext(ctx));
const rules = await caller.list();
return rules.map((rule) => ({
id: rule.id,
name: rule.name,
description: rule.description,
isActive: rule.isActive,
triggerType: rule.triggerType,
orderType: rule.orderType,
costEffect: rule.costEffect,
costReductionPercent: rule.costReductionPercent,
chargeabilityEffect: rule.chargeabilityEffect,
priority: rule.priority,
project: rule.project
? {
id: rule.project.id,
name: rule.project.name,
shortCode: rule.project.shortCode,
}
: null,
}));
},
async list_effort_rules(_params: Record<string, never>, ctx: ToolContext) {
const caller = createEffortRuleCaller(createScopedCallerContext(ctx));
const ruleSets = await caller.list();
return ruleSets.flatMap((ruleSet) => ruleSet.rules.map((rule) => ({
id: rule.id,
description: rule.description,
scopeType: rule.scopeType,
discipline: rule.discipline,
chapter: rule.chapter,
unitMode: rule.unitMode,
hoursPerUnit: rule.hoursPerUnit,
sortOrder: rule.sortOrder,
ruleSet: {
name: ruleSet.name,
isDefault: ruleSet.isDefault,
},
})));
},
async list_experience_multipliers(_params: Record<string, never>, ctx: ToolContext) {
const caller = createExperienceMultiplierCaller(createScopedCallerContext(ctx));
const multiplierSets = await caller.list();
return multiplierSets.flatMap((multiplierSet) => multiplierSet.rules.map((rule) => ({
id: rule.id,
description: rule.description,
chapter: rule.chapter,
location: rule.location,
level: rule.level,
costMultiplier: rule.costMultiplier,
billMultiplier: rule.billMultiplier,
shoringRatio: rule.shoringRatio,
additionalEffortRatio: rule.additionalEffortRatio,
sortOrder: rule.sortOrder,
multiplierSet: {
name: multiplierSet.name,
isDefault: multiplierSet.isDefault,
},
})));
},
async list_users(params: { limit?: number }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const users = await caller.list();
return users.slice(0, Math.min(params.limit ?? 50, 100));
},
async list_assignable_users(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.listAssignable();
},
async get_current_user(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.me();
},
async get_dashboard_layout(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getDashboardLayout();
},
async save_dashboard_layout(params: { layout: unknown[] }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.saveDashboardLayout({ layout: params.layout });
return {
__action: "invalidate",
scope: ["dashboard"],
success: true,
...result,
message: "Saved dashboard layout.",
};
},
async get_favorite_project_ids(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getFavoriteProjectIds();
},
async toggle_favorite_project(params: { projectId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.toggleFavoriteProject({ projectId: params.projectId });
return {
__action: "invalidate",
scope: ["project"],
success: true,
...result,
message: result.added ? "Added project to favorites." : "Removed project from favorites.",
};
},
async get_column_preferences(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getColumnPreferences();
},
async set_column_preferences(params: {
view: "resources" | "projects" | "allocations" | "vacations" | "roles" | "users" | "blueprints";
visible?: string[];
sort?: { field: string; dir: "asc" | "desc" } | null;
rowOrder?: string[] | null;
}, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.setColumnPreferences({
view: params.view,
...(params.visible !== undefined ? { visible: params.visible } : {}),
...(params.sort !== undefined ? { sort: params.sort } : {}),
...(params.rowOrder !== undefined ? { rowOrder: params.rowOrder } : {}),
});
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: `Updated column preferences for ${params.view}.`,
};
},
async generate_totp_secret(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.generateTotpSecret();
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: "Generated a new MFA TOTP secret.",
};
},
async verify_and_enable_totp(params: { token: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.verifyAndEnableTotp({ token: params.token });
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: "Enabled MFA TOTP.",
};
},
async get_mfa_status(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getMfaStatus();
},
async get_active_user_count(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.activeCount();
},
async create_user(params: {
email: string;
name: string;
systemRole?: SystemRole;
password: string;
}, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const user = await caller.create({
email: params.email,
name: params.name,
password: params.password,
...(params.systemRole !== undefined ? { systemRole: params.systemRole } : {}),
});
return {
__action: "invalidate",
scope: ["user", "resource"],
success: true,
user,
userId: user.id,
message: `Created user ${user.name}.`,
};
},
async set_user_password(params: { userId: string; password: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.setPassword(params);
return {
__action: "invalidate",
scope: ["user"],
...result,
message: `Reset password for user ${params.userId}.`,
};
},
async update_user_role(params: { id: string; systemRole: SystemRole }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const user = await caller.updateRole(params);
return {
__action: "invalidate",
scope: ["user"],
success: true,
user,
userId: user.id,
message: `Updated role for ${user.name} to ${user.systemRole}.`,
};
},
async update_user_name(params: { id: string; name: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const user = await caller.updateName(params);
return {
__action: "invalidate",
scope: ["user"],
success: true,
user,
userId: user.id,
message: `Updated user name to ${user.name}.`,
};
},
async link_user_resource(params: { userId: string; resourceId?: string | null }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.linkResource({
userId: params.userId,
resourceId: params.resourceId ?? null,
});
return {
__action: "invalidate",
scope: ["user", "resource"],
...result,
message: params.resourceId ? "Linked user to resource." : "Unlinked user resource.",
};
},
async auto_link_users_by_email(_params: Record<string, never>, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.autoLinkAllByEmail();
return {
__action: "invalidate",
scope: ["user", "resource"],
success: true,
...result,
message: `Auto-linked ${result.linked} user(s) by email.`,
};
},
async set_user_permissions(params: {
userId: string;
overrides?: {
granted?: string[];
denied?: string[];
chapterIds?: string[];
} | null;
}, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const user = await caller.setPermissions({
userId: params.userId,
overrides: params.overrides ?? null,
});
return {
__action: "invalidate",
scope: ["user"],
success: true,
user,
userId: user.id,
message: params.overrides ? "Updated user permission overrides." : "Cleared user permission overrides.",
};
},
async reset_user_permissions(params: { userId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const user = await caller.resetPermissions(params);
return {
__action: "invalidate",
scope: ["user"],
success: true,
user,
userId: user.id,
message: "Reset user permissions to role defaults.",
};
},
async get_effective_user_permissions(params: { userId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
return caller.getEffectivePermissions(params);
},
async disable_user_totp(params: { userId: string }, ctx: ToolContext) {
const caller = createUserCaller(createScopedCallerContext(ctx));
const result = await caller.disableTotp(params);
return {
__action: "invalidate",
scope: ["user"],
success: true,
...result,
message: `Disabled TOTP for user ${params.userId}.`,
};
},
async list_notifications(params: { unreadOnly?: boolean; limit?: number }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.list({
...(params.unreadOnly !== undefined ? { unreadOnly: params.unreadOnly } : {}),
...(params.limit !== undefined ? { limit: Math.min(params.limit, 100) } : {}),
});
},
async mark_notification_read(params: { notificationId?: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
await caller.markRead({
...(params.notificationId !== undefined ? { id: params.notificationId } : {}),
});
return {
__action: "invalidate",
scope: ["notification"],
success: true,
message: params.notificationId ? "Notification marked as read." : "All unread notifications marked as read.",
};
},
async get_unread_notification_count(_params: Record<string, never>, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const count = await caller.unreadCount();
return { count };
},
async create_notification(params: {
userId: string;
type: string;
title: string;
body?: string;
entityId?: string;
entityType?: string;
category?: "NOTIFICATION" | "REMINDER" | "TASK" | "APPROVAL";
priority?: "LOW" | "NORMAL" | "HIGH" | "URGENT";
link?: string;
taskStatus?: "OPEN" | "IN_PROGRESS" | "DONE" | "DISMISSED";
taskAction?: string;
assigneeId?: string;
dueDate?: string;
channel?: "in_app" | "email" | "both";
senderId?: string;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const notification = await caller.create({
userId: params.userId,
type: params.type,
title: params.title,
...(params.body !== undefined ? { body: params.body } : {}),
...(params.entityId !== undefined ? { entityId: params.entityId } : {}),
...(params.entityType !== undefined ? { entityType: params.entityType } : {}),
...(params.category !== undefined ? { category: params.category } : {}),
...(params.priority !== undefined ? { priority: params.priority } : {}),
...(params.link !== undefined ? { link: params.link } : {}),
...(params.taskStatus !== undefined ? { taskStatus: params.taskStatus } : {}),
...(params.taskAction !== undefined ? { taskAction: params.taskAction } : {}),
...(params.assigneeId !== undefined ? { assigneeId: params.assigneeId } : {}),
...(params.dueDate !== undefined ? { dueDate: new Date(params.dueDate) } : {}),
...(params.channel !== undefined ? { channel: params.channel } : {}),
...(params.senderId !== undefined ? { senderId: params.senderId } : {}),
});
return {
__action: "invalidate",
scope: ["notification"],
success: true,
notification,
notificationId: notification?.id ?? null,
message: `Created notification "${params.title}".`,
};
},
// ── DASHBOARD DETAIL ──
async get_dashboard_detail(params: { section?: string }, ctx: ToolContext) {
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getDetail({ ...(params.section ? { section: params.section } : {}) });
},
// ── PROJECT MANAGEMENT ──
async delete_project(params: { projectId: string }, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) return project;
const caller = createProjectCaller(createScopedCallerContext(ctx));
await caller.delete({ id: project.id });
return {
__action: "invalidate",
scope: ["project"],
success: true,
message: `Deleted project: ${project.name} (${project.shortCode})`,
};
},
// ── ORG UNIT MANAGEMENT ──
async create_org_unit(params: {
name: string;
shortName?: string;
level: number;
parentId?: string;
sortOrder?: number;
}, ctx: ToolContext) {
const caller = createOrgUnitCaller(createScopedCallerContext(ctx));
const ou = await caller.create(CreateOrgUnitSchema.parse(params));
return { __action: "invalidate", scope: ["orgUnit"], success: true, message: `Created org unit: ${ou.name}`, orgUnitId: ou.id, orgUnit: ou };
},
async update_org_unit(params: {
id: string;
name?: string;
shortName?: string | null;
sortOrder?: number;
isActive?: boolean;
parentId?: string | null;
}, ctx: ToolContext) {
const caller = createOrgUnitCaller(createScopedCallerContext(ctx));
const data = UpdateOrgUnitSchema.parse({
...(params.name !== undefined ? { name: params.name } : {}),
...(params.shortName !== undefined ? { shortName: params.shortName } : {}),
...(params.sortOrder !== undefined ? { sortOrder: params.sortOrder } : {}),
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
...(params.parentId !== undefined ? { parentId: params.parentId } : {}),
});
if (Object.keys(data).length === 0) return { error: "No fields to update" };
const ou = await caller.update({ id: params.id, data });
return { __action: "invalidate", scope: ["orgUnit"], success: true, message: `Updated org unit: ${ou.name}`, orgUnitId: ou.id, orgUnit: ou };
},
// ─── Cover Art ───────────────────────────────────────────────────────────
async generate_project_cover(params: { projectId: string; prompt?: string }, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
const caller = createProjectCaller(createScopedCallerContext(ctx));
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
const { coverImageUrl } = await caller.generateCover({
projectId: project.id,
...(params.prompt !== undefined ? { prompt: params.prompt } : {}),
});
return {
__action: "invalidate",
scope: ["project"],
success: true,
message: `Generated cover art for project "${project.name}"`,
coverImageUrl: coverImageUrl.slice(0, 100) + "...[truncated]",
};
},
async remove_project_cover(params: { projectId: string }, ctx: ToolContext) {
assertPermission(ctx, "manageProjects" as PermissionKey);
const caller = createProjectCaller(createScopedCallerContext(ctx));
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) {
return project;
}
await caller.removeCover({ projectId: project.id });
return { __action: "invalidate", scope: ["project"], success: true, message: `Removed cover art from project "${project.name}"` };
},
// ── TASK MANAGEMENT ──
async list_tasks(params: {
status?: "OPEN" | "IN_PROGRESS" | "DONE" | "DISMISSED";
includeAssigned?: boolean;
limit?: number;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.listTasks({
...(params.status !== undefined ? { status: params.status } : {}),
...(params.includeAssigned !== undefined ? { includeAssigned: params.includeAssigned } : {}),
...(params.limit !== undefined ? { limit: Math.min(params.limit, 100) } : {}),
});
},
async get_task_counts(_params: Record<string, never>, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.taskCounts();
},
async get_task_detail(params: { taskId: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.getTaskDetail({ id: params.taskId });
},
async update_task_status(params: { taskId: string; status: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const task = await caller.updateTaskStatus({
id: params.taskId,
status: params.status as "OPEN" | "IN_PROGRESS" | "DONE" | "DISMISSED",
});
return {
__action: "invalidate",
scope: ["notification"],
success: true,
task,
message: `Task status updated to ${task.taskStatus ?? params.status}.`,
};
},
async execute_task_action(params: { taskId: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const result = await caller.executeTaskAction({ id: params.taskId });
return {
__action: "invalidate",
scope: ["notification"],
success: true,
task: result.task,
message: result.actionResult.message,
};
},
async create_reminder(params: {
title: string;
body?: string;
remindAt: string;
recurrence?: string;
entityId?: string;
entityType?: string;
link?: string;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const reminder = await caller.createReminder({
title: params.title,
remindAt: new Date(params.remindAt),
...(params.body !== undefined ? { body: params.body } : {}),
...(params.recurrence !== undefined ? { recurrence: params.recurrence as "daily" | "weekly" | "monthly" } : {}),
...(params.entityId !== undefined ? { entityId: params.entityId } : {}),
...(params.entityType !== undefined ? { entityType: params.entityType } : {}),
...(params.link !== undefined ? { link: params.link } : {}),
});
return {
__action: "invalidate",
scope: ["notification"],
success: true,
reminder,
reminderId: reminder.id,
message: `Reminder "${params.title}" created.`,
};
},
async list_reminders(params: { limit?: number }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.listReminders({
...(params.limit !== undefined ? { limit: Math.min(params.limit, 100) } : {}),
});
},
async update_reminder(params: {
id: string;
title?: string;
body?: string;
remindAt?: string;
recurrence?: "daily" | "weekly" | "monthly" | null;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const reminder = await caller.updateReminder({
id: params.id,
...(params.title !== undefined ? { title: params.title } : {}),
...(params.body !== undefined ? { body: params.body } : {}),
...(params.remindAt !== undefined ? { remindAt: new Date(params.remindAt) } : {}),
...(params.recurrence !== undefined ? { recurrence: params.recurrence } : {}),
});
return {
__action: "invalidate",
scope: ["notification"],
success: true,
reminder,
reminderId: reminder.id,
message: `Updated reminder ${params.id}.`,
};
},
async delete_reminder(params: { id: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
await caller.deleteReminder({ id: params.id });
return {
__action: "invalidate",
scope: ["notification"],
success: true,
id: params.id,
message: `Deleted reminder ${params.id}.`,
};
},
async create_task_for_user(params: {
userId: string;
title: string;
body?: string;
priority?: string;
dueDate?: string;
taskAction?: string;
entityId?: string;
entityType?: string;
link?: string;
channel?: "in_app" | "email" | "both";
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const task = await caller.createTask({
userId: params.userId,
title: params.title,
...(params.body !== undefined ? { body: params.body } : {}),
...(params.priority !== undefined ? { priority: params.priority as "LOW" | "NORMAL" | "HIGH" | "URGENT" } : {}),
...(params.dueDate !== undefined ? { dueDate: new Date(params.dueDate) } : {}),
...(params.taskAction !== undefined ? { taskAction: params.taskAction } : {}),
...(params.entityId !== undefined ? { entityId: params.entityId } : {}),
...(params.entityType !== undefined ? { entityType: params.entityType } : {}),
...(params.link !== undefined ? { link: params.link } : {}),
...(params.channel !== undefined ? { channel: params.channel } : {}),
});
return {
__action: "invalidate",
scope: ["notification"],
success: true,
task,
taskId: task?.id ?? null,
message: `Created task "${params.title}" for ${params.userId}.`,
};
},
async assign_task(params: { id: string; assigneeId: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const task = await caller.assignTask(params);
return {
__action: "invalidate",
scope: ["notification"],
success: true,
task,
taskId: task.id,
message: `Assigned task ${params.id} to ${params.assigneeId}.`,
};
},
async send_broadcast(params: {
title: string;
body?: string;
targetType: string;
targetValue?: string;
category?: "NOTIFICATION" | "REMINDER" | "TASK" | "APPROVAL";
priority?: string;
channel?: string;
link?: string;
scheduledAt?: string;
taskAction?: string;
dueDate?: string;
}, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
const broadcast = await caller.createBroadcast({
title: params.title,
targetType: params.targetType as "user" | "role" | "project" | "orgUnit" | "all",
...(params.body !== undefined ? { body: params.body } : {}),
...(params.link !== undefined ? { link: params.link } : {}),
...(params.category !== undefined ? { category: params.category } : {}),
...(params.priority !== undefined ? { priority: params.priority as "LOW" | "NORMAL" | "HIGH" | "URGENT" } : {}),
...(params.channel !== undefined ? { channel: params.channel as "in_app" | "email" | "both" } : {}),
...(params.targetValue !== undefined ? { targetValue: params.targetValue } : {}),
...(params.scheduledAt !== undefined ? { scheduledAt: new Date(params.scheduledAt) } : {}),
...(params.taskAction !== undefined ? { taskAction: params.taskAction } : {}),
...(params.dueDate !== undefined ? { dueDate: new Date(params.dueDate) } : {}),
});
return {
__action: "invalidate",
scope: ["notification"],
success: true,
broadcast,
broadcastId: broadcast.id,
recipientCount: broadcast.recipientCount ?? 0,
message: `Broadcast "${params.title}" created.`,
};
},
async list_broadcasts(params: { limit?: number }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.listBroadcasts({
...(params.limit !== undefined ? { limit: Math.min(params.limit, 50) } : {}),
});
},
async get_broadcast_detail(params: { id: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
return caller.getBroadcastById({ id: params.id });
},
async delete_notification(params: { id: string }, ctx: ToolContext) {
const caller = createNotificationCaller(createScopedCallerContext(ctx));
await caller.delete({ id: params.id });
return {
__action: "invalidate",
scope: ["notification"],
success: true,
id: params.id,
message: `Deleted notification ${params.id}.`,
};
},
// ── INSIGHTS & ANOMALIES ──────────────────────────────────────────────────
async detect_anomalies(_params: Record<string, never>, ctx: ToolContext) {
const caller = createInsightsCaller(createScopedCallerContext(ctx));
return caller.getAnomalyDetail();
},
async get_skill_gaps(_params: Record<string, never>, ctx: ToolContext) {
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getSkillGapSummary();
},
async get_project_health(_params: Record<string, never>, ctx: ToolContext) {
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getProjectHealthDetail();
},
async get_budget_forecast(_params: Record<string, never>, ctx: ToolContext) {
assertPermission(ctx, "viewCosts" as PermissionKey);
const caller = createDashboardCaller(createScopedCallerContext(ctx));
return caller.getBudgetForecastDetail();
},
async get_insights_summary(_params: Record<string, never>, ctx: ToolContext) {
const caller = createInsightsCaller(createScopedCallerContext(ctx));
return caller.getInsightsSummary();
},
async run_report(params: {
entity: string;
columns: string[];
filters?: Array<{
field: string;
op: "eq" | "neq" | "gt" | "lt" | "gte" | "lte" | "contains" | "in";
value: string;
}>;
periodMonth?: string;
limit?: number;
}, ctx: ToolContext) {
const entity = params.entity as "resource" | "project" | "assignment" | "resource_month";
if (!["resource", "project", "assignment", "resource_month"].includes(entity)) {
return {
error:
`Unknown entity: ${params.entity}. Use resource, project, assignment, or resource_month.`,
};
}
const caller = createReportCaller(createScopedCallerContext(ctx));
const result = await caller.getReportData({
entity,
columns: params.columns,
filters: params.filters ?? [],
periodMonth: params.periodMonth,
sortDir: "asc",
limit: Math.min(params.limit ?? 50, 200),
offset: 0,
});
return {
rows: result.rows,
rowCount: result.rows.length,
columns: result.columns,
};
},
async list_comments(params: { entityType: string; entityId: string }, ctx: ToolContext) {
const caller = createCommentCaller(createScopedCallerContext(ctx));
const comments = await caller.list({
entityType: params.entityType,
entityId: params.entityId,
});
return comments.map((c) => ({
id: c.id,
author: c.author.name ?? c.author.email,
body: c.body,
resolved: c.resolved,
createdAt: c.createdAt.toISOString(),
replyCount: c.replies.length,
replies: c.replies.map((r) => ({
id: r.id,
author: r.author.name ?? r.author.email,
body: r.body,
resolved: r.resolved,
createdAt: r.createdAt.toISOString(),
})),
}));
},
async lookup_rate(params: {
clientId?: string;
chapter?: string;
managementLevelId?: string;
roleName?: string;
seniority?: string;
}, ctx: ToolContext) {
assertPermission(ctx, "viewCosts" as PermissionKey);
const caller = createRateCardCaller(createScopedCallerContext(ctx));
const result = await caller.lookupBestMatch(params);
return {
...(result.message ? { message: result.message } : {}),
bestMatch: result.bestMatch
? {
...result.bestMatch,
costRate: fmtEur(result.bestMatch.costRateCents),
billRate: result.bestMatch.billRateCents ? fmtEur(result.bestMatch.billRateCents) : null,
}
: null,
alternatives: result.alternatives.map((alternative) => ({
...alternative,
costRate: fmtEur(alternative.costRateCents),
billRate: alternative.billRateCents ? fmtEur(alternative.billRateCents) : null,
})),
totalCandidates: result.totalCandidates,
};
},
// ── SCENARIO & AI ─────────────────────────────────────────────────────────
async simulate_scenario(params: {
projectId: string;
changes: Array<{
assignmentId?: string;
resourceId?: string;
roleId?: string;
startDate: string;
endDate: string;
hoursPerDay: number;
remove?: boolean;
}>;
}, ctx: ToolContext) {
const caller = createScenarioCaller(createScopedCallerContext(ctx));
const result = await caller.simulate({
projectId: params.projectId,
changes: params.changes.map((change) => ({
...change,
startDate: new Date(change.startDate),
endDate: new Date(change.endDate),
})),
});
return {
baseline: {
...result.baseline,
totalCost: fmtEur(result.baseline.totalCostCents),
},
scenario: {
...result.scenario,
totalCost: fmtEur(result.scenario.totalCostCents),
},
delta: {
...result.delta,
cost: fmtEur(result.delta.costCents),
},
resourceImpacts: result.resourceImpacts,
warnings: result.warnings,
budgetCents: result.budgetCents,
};
},
async generate_project_narrative(params: { projectId: string }, ctx: ToolContext) {
const caller = createInsightsCaller(createScopedCallerContext(ctx));
return caller.generateProjectNarrative({ projectId: params.projectId });
},
async create_comment(params: {
entityType: string;
entityId: string;
body: string;
}, ctx: ToolContext) {
const caller = createCommentCaller(createScopedCallerContext(ctx));
const comment = await caller.create({
entityType: params.entityType,
entityId: params.entityId,
body: params.body,
});
return {
__action: "invalidate",
scope: ["comment"],
id: comment.id,
author: comment.author.name ?? comment.author.email,
body: comment.body,
createdAt: comment.createdAt.toISOString(),
};
},
async resolve_comment(params: {
commentId: string;
resolved?: boolean;
}, ctx: ToolContext) {
const caller = createCommentCaller(createScopedCallerContext(ctx));
const updated = await caller.resolve({
id: params.commentId,
resolved: params.resolved !== false,
});
return {
__action: "invalidate",
scope: ["comment"],
id: updated.id,
resolved: updated.resolved,
author: updated.author.name ?? updated.author.email,
body: updated.body.slice(0, 100),
};
},
async query_change_history(params: {
entityType?: string;
search?: string;
userId?: string;
daysBack?: number;
action?: string;
limit?: number;
}, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 20, 50);
const daysBack = params.daysBack ?? 7;
const startDate = new Date();
startDate.setDate(startDate.getDate() - daysBack);
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
const result = await caller.listDetail({
...(params.entityType ? { entityType: params.entityType } : {}),
...(params.userId ? { userId: params.userId } : {}),
...(params.action ? { action: params.action } : {}),
...(params.search ? { search: params.search } : {}),
startDate,
limit,
});
return {
filters: {
entityType: params.entityType ?? null,
userId: params.userId ?? null,
action: params.action ?? null,
search: params.search ?? null,
daysBack,
},
itemCount: result.items.length,
nextCursor: result.nextCursor ?? null,
items: result.items,
};
},
async get_entity_timeline(params: {
entityType: string;
entityId: string;
limit?: number;
}, ctx: ToolContext) {
const limit = Math.min(params.limit ?? 50, 200);
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
return caller.getByEntityDetail({
entityType: params.entityType,
entityId: params.entityId,
limit,
});
},
async export_resources_csv(_params: Record<string, never>, ctx: ToolContext) {
const caller = createImportExportCaller(createScopedCallerContext(ctx));
const csv = await caller.exportResourcesCSV();
return {
format: "csv",
lineCount: csv.length === 0 ? 0 : csv.split("\n").length,
csv,
};
},
async export_projects_csv(_params: Record<string, never>, ctx: ToolContext) {
const caller = createImportExportCaller(createScopedCallerContext(ctx));
const csv = await caller.exportProjectsCSV();
return {
format: "csv",
lineCount: csv.length === 0 ? 0 : csv.split("\n").length,
csv,
};
},
async import_csv_data(params: {
entityType: "resources" | "projects" | "allocations";
rows: Array<Record<string, string>>;
dryRun?: boolean;
}, ctx: ToolContext) {
assertPermission(ctx, PermissionKey.IMPORT_DATA);
const caller = createImportExportCaller(createScopedCallerContext(ctx));
return caller.importCSV({
entityType: params.entityType,
rows: params.rows,
dryRun: params.dryRun ?? true,
});
},
async list_dispo_import_batches(params: {
status?: ImportBatchStatus;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listImportBatches({
...(params.status ? { status: params.status } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async get_dispo_import_batch(params: {
id: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.getImportBatch({ id: params.id });
},
async stage_dispo_import_batch(params: {
chargeabilityWorkbookPath: string;
costWorkbookPath?: string;
notes?: string | null;
planningWorkbookPath: string;
referenceWorkbookPath: string;
rosterWorkbookPath?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.stageImportBatch({
chargeabilityWorkbookPath: params.chargeabilityWorkbookPath,
planningWorkbookPath: params.planningWorkbookPath,
referenceWorkbookPath: params.referenceWorkbookPath,
...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}),
...(params.notes !== undefined ? { notes: params.notes } : {}),
...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}),
});
},
async validate_dispo_import_batch(params: {
chargeabilityWorkbookPath: string;
costWorkbookPath?: string;
importBatchId?: string;
notes?: string | null;
planningWorkbookPath: string;
referenceWorkbookPath: string;
rosterWorkbookPath?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.validateImportBatch({
chargeabilityWorkbookPath: params.chargeabilityWorkbookPath,
planningWorkbookPath: params.planningWorkbookPath,
referenceWorkbookPath: params.referenceWorkbookPath,
...(params.costWorkbookPath !== undefined ? { costWorkbookPath: params.costWorkbookPath } : {}),
...(params.importBatchId !== undefined ? { importBatchId: params.importBatchId } : {}),
...(params.notes !== undefined ? { notes: params.notes } : {}),
...(params.rosterWorkbookPath !== undefined ? { rosterWorkbookPath: params.rosterWorkbookPath } : {}),
});
},
async cancel_dispo_import_batch(params: {
id: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.cancelImportBatch({ id: params.id });
},
async list_dispo_staged_resources(params: {
importBatchId: string;
status?: StagedRecordStatus;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedResources({
importBatchId: params.importBatchId,
...(params.status !== undefined ? { status: params.status } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async list_dispo_staged_projects(params: {
importBatchId: string;
status?: StagedRecordStatus;
isTbd?: boolean;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedProjects({
importBatchId: params.importBatchId,
...(params.status !== undefined ? { status: params.status } : {}),
...(params.isTbd !== undefined ? { isTbd: params.isTbd } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async list_dispo_staged_assignments(params: {
importBatchId: string;
status?: StagedRecordStatus;
resourceExternalId?: string;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedAssignments({
importBatchId: params.importBatchId,
...(params.status !== undefined ? { status: params.status } : {}),
...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async list_dispo_staged_vacations(params: {
importBatchId: string;
resourceExternalId?: string;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedVacations({
importBatchId: params.importBatchId,
...(params.resourceExternalId !== undefined ? { resourceExternalId: params.resourceExternalId } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async list_dispo_staged_unresolved_records(params: {
importBatchId: string;
recordType?: DispoStagedRecordType;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.listStagedUnresolvedRecords({
importBatchId: params.importBatchId,
...(params.recordType !== undefined ? { recordType: params.recordType } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 200) } : {}),
});
},
async resolve_dispo_staged_record(params: {
action: "APPROVE" | "REJECT" | "SKIP";
id: string;
recordType: DispoStagedRecordType;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.resolveStagedRecord({
action: params.action,
id: params.id,
recordType: params.recordType,
});
},
async commit_dispo_import_batch(params: {
allowTbdUnresolved?: boolean;
importBatchId: string;
importTbdProjects?: boolean;
}, ctx: ToolContext) {
const caller = createDispoCaller(createScopedCallerContext(ctx));
return caller.commitImportBatch({
importBatchId: params.importBatchId,
...(params.allowTbdUnresolved !== undefined ? { allowTbdUnresolved: params.allowTbdUnresolved } : {}),
...(params.importTbdProjects !== undefined ? { importTbdProjects: params.importTbdProjects } : {}),
});
},
async get_system_settings(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.getSystemSettings();
},
async update_system_settings(params: {
aiProvider?: "openai" | "azure";
azureOpenAiEndpoint?: string;
azureOpenAiDeployment?: string;
azureOpenAiApiKey?: string;
azureApiVersion?: string;
aiMaxCompletionTokens?: number;
aiTemperature?: number;
aiSummaryPrompt?: string;
scoreWeights?: {
skillDepth: number;
skillBreadth: number;
costEfficiency: number;
chargeability: number;
experience: number;
};
scoreVisibleRoles?: SystemRole[];
smtpHost?: string;
smtpPort?: number;
smtpUser?: string;
smtpPassword?: string;
smtpFrom?: string;
smtpTls?: boolean;
anonymizationEnabled?: boolean;
anonymizationDomain?: string;
anonymizationSeed?: string;
anonymizationMode?: "global";
azureDalleDeployment?: string;
azureDalleEndpoint?: string;
azureDalleApiKey?: string;
geminiApiKey?: string;
geminiModel?: string;
imageProvider?: "dalle" | "gemini";
vacationDefaultDays?: number;
timelineUndoMaxSteps?: number;
}, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.updateSystemSettings(params);
},
async test_ai_connection(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.testAiConnection();
},
async test_smtp_connection(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.testSmtpConnection();
},
async test_gemini_connection(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.testGeminiConnection();
},
async get_ai_configured(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSettingsCaller(createScopedCallerContext(ctx));
return caller.getAiConfigured();
},
async list_system_role_configs(_params: Record<string, never>, ctx: ToolContext) {
const caller = createSystemRoleConfigCaller(createScopedCallerContext(ctx));
return caller.list();
},
async update_system_role_config(params: {
role: string;
label?: string;
description?: string | null;
color?: string | null;
defaultPermissions?: string[];
}, ctx: ToolContext) {
const caller = createSystemRoleConfigCaller(createScopedCallerContext(ctx));
return caller.update(params);
},
async list_webhooks(_params: Record<string, never>, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
const webhooks = await caller.list();
return sanitizeWebhookList(webhooks);
},
async get_webhook(params: {
id: string;
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
const webhook = await caller.getById({ id: params.id });
return sanitizeWebhook(webhook);
},
async create_webhook(params: {
name: string;
url: string;
secret?: string;
events: string[];
isActive?: boolean;
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
const webhook = await caller.create({
name: params.name,
url: params.url,
events: params.events as [string, ...string[]],
...(params.secret !== undefined ? { secret: params.secret } : {}),
...(params.isActive !== undefined ? { isActive: params.isActive } : {}),
});
return sanitizeWebhook(webhook);
},
async update_webhook(params: {
id: string;
data: {
name?: string;
url?: string;
secret?: string | null;
events?: string[];
isActive?: boolean;
};
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
const webhook = await caller.update({
id: params.id,
data: {
...(params.data.name !== undefined ? { name: params.data.name } : {}),
...(params.data.url !== undefined ? { url: params.data.url } : {}),
...(params.data.secret !== undefined ? { secret: params.data.secret } : {}),
...(params.data.events !== undefined ? { events: params.data.events as [string, ...string[]] } : {}),
...(params.data.isActive !== undefined ? { isActive: params.data.isActive } : {}),
},
});
return sanitizeWebhook(webhook);
},
async delete_webhook(params: {
id: string;
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
await caller.delete({ id: params.id });
return { ok: true, id: params.id };
},
async test_webhook(params: {
id: string;
}, ctx: ToolContext) {
const caller = createWebhookCaller(createScopedCallerContext(ctx));
return caller.test({ id: params.id });
},
async list_audit_log_entries(params: {
entityType?: string;
entityId?: string;
userId?: string;
action?: string;
source?: string;
startDate?: string;
endDate?: string;
search?: string;
limit?: number;
cursor?: string;
}, ctx: ToolContext) {
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
const result = await caller.listDetail({
...(params.entityType ? { entityType: params.entityType } : {}),
...(params.entityId ? { entityId: params.entityId } : {}),
...(params.userId ? { userId: params.userId } : {}),
...(params.action ? { action: params.action } : {}),
...(params.source ? { source: params.source } : {}),
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
...(params.search ? { search: params.search } : {}),
...(params.cursor ? { cursor: params.cursor } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 100) } : {}),
});
return {
filters: {
entityType: params.entityType ?? null,
entityId: params.entityId ?? null,
userId: params.userId ?? null,
action: params.action ?? null,
source: params.source ?? null,
startDate: params.startDate ?? null,
endDate: params.endDate ?? null,
search: params.search ?? null,
},
itemCount: result.items.length,
nextCursor: result.nextCursor ?? null,
items: result.items,
};
},
async get_audit_log_entry(params: {
id: string;
}, ctx: ToolContext) {
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
return caller.getByIdDetail({ id: params.id });
},
async get_audit_log_timeline(params: {
startDate?: string;
endDate?: string;
limit?: number;
}, ctx: ToolContext) {
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
return caller.getTimelineDetail({
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
...(params.limit !== undefined ? { limit: Math.min(Math.max(params.limit, 1), 500) } : {}),
});
},
async get_audit_activity_summary(params: {
startDate?: string;
endDate?: string;
}, ctx: ToolContext) {
const caller = createAuditLogCaller(createScopedCallerContext(ctx));
return caller.getActivitySummary({
...(params.startDate ? { startDate: parseIsoDate(params.startDate, "startDate") } : {}),
...(params.endDate ? { endDate: parseIsoDate(params.endDate, "endDate") } : {}),
});
},
async get_shoring_ratio(params: { projectId: string }, ctx: ToolContext) {
const project = await resolveProjectIdentifier(ctx, params.projectId);
if ("error" in project) return project;
const caller = createProjectCaller(createScopedCallerContext(ctx));
const result = await caller.getShoringRatio({ projectId: project.id });
if (result.totalHours <= 0) {
return `Project "${project.name}" (${project.shortCode}): No active assignments — shoring ratio not available.`;
}
const countryParts = Object.entries(result.byCountry)
.sort((a, b) => b[1].pct - a[1].pct)
.map(([code, info]) => `${code} ${info.pct}% (${info.resourceCount} people)`)
.join(", ");
const status = result.offshoreRatio >= result.threshold
? `Target met (>=${result.threshold}% offshore)`
: result.offshoreRatio >= result.threshold - 10
? `Close to target (${result.threshold}% offshore needed)`
: `Below target — only ${result.offshoreRatio}% offshore, need ${result.threshold}%`;
return `Project "${project.name}" (${project.shortCode}): ${result.onshoreRatio}% onshore (${result.onshoreCountryCode}), ${result.offshoreRatio}% offshore. ${status}. Breakdown: ${countryParts}.${result.unknownCount > 0 ? ` (${result.unknownCount} resource(s) without country)` : ""}`;
},
};
// ─── Executor ───────────────────────────────────────────────────────────────
export interface ToolAction {
type: string;
url?: string;
scope?: string[];
description?: string;
}
export interface ToolResult {
content: string;
action?: ToolAction;
data?: unknown;
}
export async function executeTool(
name: string,
args: string,
ctx: ToolContext,
): Promise<ToolResult> {
const executor = executors[name as keyof typeof executors];
if (!executor) return { content: JSON.stringify({ error: `Unknown tool: ${name}` }) };
try {
const params = JSON.parse(args);
// Audit-log all mutation tool executions (EGAI 4.1.3.1 / IAAI 3.6.26)
if (MUTATION_TOOLS.has(name)) {
logger.info(
{ tool: name, params, userId: ctx.userId, userRole: ctx.userRole },
"AI assistant mutation tool executed",
);
}
const result = await executor(params, ctx);
// Detect action payloads (e.g. navigation, invalidation)
if (result && typeof result === "object" && "__action" in (result as Record<string, unknown>)) {
const actionResult = result as Record<string, unknown>;
const actionType = actionResult.__action as string;
if (actionType === "navigate") {
const url = actionResult.url as string;
const desc = (actionResult.description as string | undefined) ?? url;
return {
content: JSON.stringify({ description: desc }),
data: { description: desc },
action: { type: "navigate", url, description: desc },
};
}
if (actionType === "invalidate") {
const scope = actionResult.scope as string[];
// Strip __action, scope, and large data from the result sent back to the AI
const { __action: _, scope: _s, coverImageUrl: _img, ...rest } = actionResult;
const content = JSON.stringify(rest);
return {
content: content.length > 4000 ? content.slice(0, 4000) + '..."' : content,
data: rest,
action: { type: "invalidate", scope },
};
}
}
// Cap tool result size to prevent oversized OpenAI conversation payloads
const content = typeof result === "string" ? result : JSON.stringify(result);
return {
content: content.length > 8000 ? content.slice(0, 8000) + '..."' : content,
...(typeof result === "string" ? {} : { data: result }),
};
} catch (err) {
const msg = err instanceof Error ? err.message : String(err);
logger.error(
{
tool: name,
userId: ctx.userId,
userRole: ctx.userRole,
error: err instanceof Error ? { message: err.message, stack: err.stack } : err,
},
"AI assistant tool execution failed",
);
return { content: JSON.stringify({ error: msg }) };
}
}