@t4dhg/mcp-factorial 10.1.0 → 10.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +35 -25
  2. package/dist/api/documents.js +11 -11
  3. package/dist/api/documents.js.map +1 -1
  4. package/dist/api/index.d.ts +1 -1
  5. package/dist/api/index.d.ts.map +1 -1
  6. package/dist/api/index.js +1 -1
  7. package/dist/api/index.js.map +1 -1
  8. package/dist/api/time-off.d.ts +6 -0
  9. package/dist/api/time-off.d.ts.map +1 -1
  10. package/dist/api/time-off.js +16 -5
  11. package/dist/api/time-off.js.map +1 -1
  12. package/dist/attendance/backfill.d.ts +9 -3
  13. package/dist/attendance/backfill.d.ts.map +1 -1
  14. package/dist/attendance/backfill.js +19 -6
  15. package/dist/attendance/backfill.js.map +1 -1
  16. package/dist/attendance/gate.js +1 -1
  17. package/dist/attendance/gate.js.map +1 -1
  18. package/dist/attendance/planner.d.ts +30 -2
  19. package/dist/attendance/planner.d.ts.map +1 -1
  20. package/dist/attendance/planner.js +66 -9
  21. package/dist/attendance/planner.js.map +1 -1
  22. package/dist/attendance/report.d.ts +36 -0
  23. package/dist/attendance/report.d.ts.map +1 -0
  24. package/dist/attendance/report.js +62 -0
  25. package/dist/attendance/report.js.map +1 -0
  26. package/dist/confirmation.d.ts.map +1 -1
  27. package/dist/confirmation.js +3 -2
  28. package/dist/confirmation.js.map +1 -1
  29. package/dist/http-client.d.ts +45 -1
  30. package/dist/http-client.d.ts.map +1 -1
  31. package/dist/http-client.js +47 -3
  32. package/dist/http-client.js.map +1 -1
  33. package/dist/prompts/attendance.d.ts +38 -0
  34. package/dist/prompts/attendance.d.ts.map +1 -0
  35. package/dist/prompts/attendance.js +366 -0
  36. package/dist/prompts/attendance.js.map +1 -0
  37. package/dist/tools/attendance.d.ts.map +1 -1
  38. package/dist/tools/attendance.js +61 -40
  39. package/dist/tools/attendance.js.map +1 -1
  40. package/dist/tools/index.d.ts.map +1 -1
  41. package/dist/tools/index.js +4 -1
  42. package/dist/tools/index.js.map +1 -1
  43. package/llms.txt +3 -1
  44. package/package.json +1 -1
@@ -0,0 +1,366 @@
1
+ /**
2
+ * Attendance prompts and the registro horario guide.
3
+ *
4
+ * A prompt is a procedure the user invokes (a slash command in Claude Code,
5
+ * an entry in the prompt menu elsewhere). Each one here pre-reads the facts
6
+ * the procedure starts from, states the exact tool calls that follow with
7
+ * their arguments filled in, and spells out the decision rules, so that a
8
+ * small model can carry the workflow through without having to design it.
9
+ * Nothing is written by a prompt itself; writes happen only through the
10
+ * factorial_attendance tool and its confirmation gate.
11
+ *
12
+ * The guide resource (factorial://guides/registro-horario) is the same
13
+ * knowledge for a model that discovers resources on its own.
14
+ */
15
+ import * as z from 'zod';
16
+ import { localToday } from '../api/attendance.js';
17
+ import { buildLedger, findGaps } from '../attendance/backfill.js';
18
+ import { resolveEmployeeName, resolveTargetEmployeeId } from '../attendance/identity.js';
19
+ import { DEFAULT_TOLERANCE_MINUTES, hours, validateSegments, } from '../attendance/planner.js';
20
+ import { formatAudit, formatGaps, ledgerRow } from '../attendance/report.js';
21
+ export const GUIDE_URI = 'factorial://guides/registro-horario';
22
+ const SEGMENT = /^([01]\d|2[0-3]):[0-5]\d-([01]\d|2[0-3]):[0-5]\d$/;
23
+ /**
24
+ * Parse the `segments` prompt argument. Prompt arguments are strings, so the
25
+ * daily pattern arrives as "09:00-14:00, 15:00-18:00" (comma, semicolon or
26
+ * whitespace separated) or as the JSON array the tool takes.
27
+ */
28
+ export function parseSegmentsArg(text) {
29
+ const trimmed = text.trim();
30
+ if (trimmed === '') {
31
+ throw new Error('segments is required, for example "09:00-14:00, 15:00-18:00"');
32
+ }
33
+ let segments;
34
+ if (trimmed.startsWith('[')) {
35
+ const parsed = JSON.parse(trimmed);
36
+ if (!Array.isArray(parsed))
37
+ throw new Error('segments JSON must be an array');
38
+ segments = parsed.map((item) => {
39
+ if (item === null ||
40
+ typeof item !== 'object' ||
41
+ typeof item.clock_in !== 'string' ||
42
+ typeof item.clock_out !== 'string') {
43
+ throw new Error('each segment needs clock_in and clock_out as "HH:MM" strings');
44
+ }
45
+ return { clock_in: item.clock_in, clock_out: item.clock_out };
46
+ });
47
+ }
48
+ else {
49
+ segments = trimmed
50
+ .split(/[,;\s]+/)
51
+ .filter(part => part !== '')
52
+ .map(part => {
53
+ if (!SEGMENT.test(part)) {
54
+ throw new Error(`Segment "${part}" must be HH:MM-HH:MM in 24-hour company local time, ` +
55
+ 'for example "09:00-14:00, 15:00-18:00"');
56
+ }
57
+ const [clock_in, clock_out] = part.split('-');
58
+ return { clock_in, clock_out };
59
+ });
60
+ }
61
+ validateSegments(segments);
62
+ return segments;
63
+ }
64
+ /** Sum of a daily pattern in minutes */
65
+ function patternMinutes(segments) {
66
+ return segments.reduce((sum, s) => {
67
+ const [ih, im] = s.clock_in.split(':').map(Number);
68
+ const [oh, om] = s.clock_out.split(':').map(Number);
69
+ return sum + (oh * 60 + om) - (ih * 60 + im);
70
+ }, 0);
71
+ }
72
+ function parseOptionalInt(value, name) {
73
+ if (value === undefined || value.trim() === '')
74
+ return undefined;
75
+ const n = Number(value);
76
+ if (!Number.isInteger(n) || n < 0)
77
+ throw new Error(`${name} must be a non-negative integer`);
78
+ return n;
79
+ }
80
+ /** First day of the month of `today` up to `today`, the default audit window */
81
+ export function defaultWindow(today) {
82
+ return { start_on: `${today.slice(0, 7)}-01`, end_on: today };
83
+ }
84
+ function resolveWindow(startOn, endOn, today) {
85
+ const fallback = defaultWindow(today);
86
+ return {
87
+ start_on: startOn && startOn.trim() !== '' ? startOn.trim() : fallback.start_on,
88
+ end_on: endOn && endOn.trim() !== '' ? endOn.trim() : fallback.end_on,
89
+ };
90
+ }
91
+ function toolCall(args) {
92
+ return `factorial_attendance(${JSON.stringify(args)})`;
93
+ }
94
+ function userMessage(text) {
95
+ return { messages: [{ role: 'user', content: { type: 'text', text } }] };
96
+ }
97
+ /** Pre-read the data a procedure starts from; on failure, say so instead of failing the prompt */
98
+ async function preRead(label, read) {
99
+ try {
100
+ return await read();
101
+ }
102
+ catch (error) {
103
+ const message = error instanceof Error ? error.message : String(error);
104
+ return (`The server could not pre-read ${label}: ${message}\n` +
105
+ 'Run the corresponding factorial_attendance call yourself before continuing.');
106
+ }
107
+ }
108
+ const STATUS_LEGEND = [
109
+ 'Statuses: complete (tracked within tolerance of expected), missing (hours short), over (hours in',
110
+ 'excess), weekend, bank_holiday, on_leave (approved full-day leave), half_day_leave (approved half',
111
+ 'day; the other half may be missing), not_workable (the contract expects 0 minutes), no_contract_data',
112
+ '(Factorial returned nothing for the date; usually before the start of employment, never a fact about',
113
+ 'hours), future (after today, never written).',
114
+ ].join(' ');
115
+ export const REGISTRO_HORARIO_GUIDE = `# Registro horario with factorial_attendance
116
+
117
+ This guide is for the model driving the tool. Every step below is a call to the \`factorial_attendance\` tool with an \`action\`. Times are HH:MM in company local time. Dates are YYYY-MM-DD.
118
+
119
+ ## What the data means
120
+
121
+ - **Expected** minutes come from the employee's contract pattern (\`estimated_times\`).
122
+ - **Tracked** minutes come from the shift records on file (\`worked_times\`, \`shifts\`).
123
+ - **Day type** and bank holidays come from the company calendar in Factorial (\`worked_times.day_type\`).
124
+ - **Leave** comes from approved \`timeoff/leaves\` records. Pending or rejected leave does not count.
125
+ - ${STATUS_LEGEND}
126
+ - Every \`gaps\`, \`audit\` and bulk preview starts with a **Data read** line saying how many days of the window have contract data and how many leave and shift records were read. If it reports uncovered days that do not precede the start of employment, stop and report; do not write.
127
+ - The server reads every page of every list, so a window of any length is read completely. There is no need to split a range into chunks unless the result is too large to read; if it is, use \`format: "summary"\` on \`audit\` or split by month.
128
+
129
+ ## Workflow A: audit and report
130
+
131
+ 1. \`audit\` with \`start_on\`, \`end_on\` (default \`format\` is \`summary\`: only the days needing attention).
132
+ 2. Read the Data read line first. Then report: expected vs tracked for the window, the list of \`missing\` days with how much is missing on each, any \`over\` days, any \`half_day_leave\` days whose worked half is untracked, and any \`no_contract_data\` days with the warning above.
133
+ 3. Write nothing. An audit is read-only.
134
+
135
+ ## Workflow B: fill what is missing
136
+
137
+ 1. \`gaps\` for the window to see which workdays are short and by how much.
138
+ 2. Ask the person for their usual daily pattern if you do not have it, as segments such as \`[{"clock_in":"09:00","clock_out":"14:00"},{"clock_in":"15:00","clock_out":"18:00"}]\`. Never invent hours; the record is a legal document of time actually worked.
139
+ 3. \`log_range\` with \`start_on\`, \`end_on\`, \`segments\`, and usually \`jitter_minutes\` (5 to 10) and an \`observations\` note saying why the records are being entered now. The first call returns a **preview** and a \`confirmation_token\`; nothing is written. The preview skips weekends, bank holidays, approved leave, days the contract does not expect work, future dates, days without contract data, and any segment overlapping an existing shift.
140
+ 4. Show the preview to the person and ask them to confirm. Only then repeat the identical call with \`confirmation_token\`. The token lasts 15 minutes and matches exactly that plan. A bulk write always needs this step; there is no way around it and you must not look for one.
141
+ 5. Days the person did not work in the range (sick without a leave record, a day off) must be excluded: either narrow the range, or run \`log_range\` on the sub-ranges around them. Days worked outside the pattern (a Saturday, a half day) are written one by one with \`log_days\`, which takes explicit dates and segments and skips only future dates, approved leave and overlaps.
142
+ 6. If the write stops part way, the result says where. Re-running the identical call is safe: the planner re-reads existing shifts and writes only what is still missing.
143
+ 7. \`audit\` the same window again and report what changed. Every workday should now read \`complete\`.
144
+
145
+ ## Workflow C: today's record (for a daily routine)
146
+
147
+ 1. \`status\` to see whether the employee is clocked in. If a shift is open, do not write; report it.
148
+ 2. \`audit\` with \`start_on\` and \`end_on\` both set to today. If the status for today is \`complete\`, \`weekend\`, \`bank_holiday\`, \`on_leave\` or \`not_workable\`, there is nothing to do; report in one line.
149
+ 3. If today is \`missing\`, \`log_days\` with \`days: [{"date": today, "segments": [...]}]\`, the person's stated pattern, and \`jitter_minutes\`. Then repeat the same call with the returned \`confirmation_token\` in the same session, only if the target is the configured identity (\`FACTORIAL_EMPLOYEE_ID\`). For anyone else, stop and ask.
150
+ 4. Report in one line what was written or why nothing was.
151
+
152
+ MCP has no scheduler. To run this daily, schedule it in the client: in Claude Code, \`/schedule\` creates a routine that invokes the \`attendance_today\` prompt, and \`/loop\` repeats it while a session is open; any cron can run \`claude -p\` with the prompt as its input.
153
+
154
+ ## Rules that always apply
155
+
156
+ - Never write hours for someone other than the person asking unless they name that employee explicitly; the tool requires a confirmation token for it, and so should you.
157
+ - Never pass a \`confirmation_token\` without the person having seen the preview it belongs to, except in Workflow C for the configured identity.
158
+ - Never delete or overwrite records to make an audit look right. Report discrepancies instead.
159
+ - Prefer \`jitter_minutes\` when reconstructing approximate hours, so a month does not read 09:00 on every line. Records still carry \`source: "api"\`.
160
+ - The records carry the \`observations\` note; write in it why they were entered (for example "Entered from calendar records on 2026-09-06").
161
+ `;
162
+ /**
163
+ * Register the three attendance prompts and the guide resource on a server.
164
+ * `deps.now` exists for tests; the server uses the wall clock.
165
+ */
166
+ export function registerAttendancePrompts(server, deps = {}) {
167
+ const today = () => localToday((deps.now ?? (() => new Date()))());
168
+ server.registerResource('registro_horario_guide', GUIDE_URI, {
169
+ title: 'Registro horario guide',
170
+ description: "How to audit, fill and maintain an employee's registro horario (attendance record) with " +
171
+ 'the factorial_attendance tool: what the data means, the three workflows, and the rules.',
172
+ mimeType: 'text/markdown',
173
+ }, uri => ({
174
+ contents: [{ uri: uri.href, mimeType: 'text/markdown', text: REGISTRO_HORARIO_GUIDE }],
175
+ }));
176
+ server.registerPrompt('attendance_audit', {
177
+ title: 'Audit registro horario',
178
+ description: "Audit an employee's registro horario for a date window and report what is missing, in " +
179
+ 'excess or inconsistent. Read-only. Defaults to the current month up to today and to ' +
180
+ 'FACTORIAL_EMPLOYEE_ID.',
181
+ argsSchema: {
182
+ start_on: z
183
+ .string()
184
+ .optional()
185
+ .describe('Start date YYYY-MM-DD (default: 1st of this month)'),
186
+ end_on: z.string().optional().describe('End date YYYY-MM-DD (default: today)'),
187
+ employee_id: z.string().optional().describe('Employee ID (default: FACTORIAL_EMPLOYEE_ID)'),
188
+ },
189
+ }, async ({ start_on, end_on, employee_id }) => {
190
+ const employeeId = resolveTargetEmployeeId(parseOptionalInt(employee_id, 'employee_id'));
191
+ const name = await resolveEmployeeName(employeeId);
192
+ const window = resolveWindow(start_on, end_on, today());
193
+ const report = await preRead('the audit', async () => {
194
+ const { ledger, coverage } = await buildLedger(employeeId, window.start_on, window.end_on);
195
+ return formatAudit({
196
+ employee: { id: employeeId, name },
197
+ startOn: window.start_on,
198
+ endOn: window.end_on,
199
+ ledger,
200
+ coverage,
201
+ toleranceMinutes: DEFAULT_TOLERANCE_MINUTES,
202
+ format: 'summary',
203
+ });
204
+ });
205
+ return userMessage([
206
+ `Audit the registro horario of ${name} (${employeeId}) from ${window.start_on} to ${window.end_on} and report on it.`,
207
+ '',
208
+ 'The audit has already been run for you:',
209
+ '',
210
+ report,
211
+ '',
212
+ 'Do this:',
213
+ '1. Read the Data read line. If it names days without contract data that are not before the start of employment, say so first and treat the audit as unreliable.',
214
+ '2. Report expected vs tracked hours for the window and the count of days per status.',
215
+ '3. List every missing day with the hours missing on it, grouped by month. List over days. Mention half_day_leave days whose worked half has no record.',
216
+ '4. If nothing needs attention, say so in one line.',
217
+ '5. Write nothing. This is a read-only report. If the person then wants to fill the gaps, use the attendance_fill prompt or follow Workflow B in the guide (' +
218
+ GUIDE_URI +
219
+ ').',
220
+ '',
221
+ `To see every day rather than only the exceptions: ${toolCall({ action: 'audit', employee_id: employeeId, start_on: window.start_on, end_on: window.end_on, format: 'table' })}`,
222
+ STATUS_LEGEND,
223
+ ].join('\n'));
224
+ });
225
+ server.registerPrompt('attendance_fill', {
226
+ title: 'Fill missing registro horario',
227
+ description: "Find the workdays with missing hours in a window and enter them with the employee's daily " +
228
+ 'pattern, through a preview the person confirms. Defaults to the current month up to today ' +
229
+ 'and to FACTORIAL_EMPLOYEE_ID.',
230
+ argsSchema: {
231
+ segments: z
232
+ .string()
233
+ .describe('Daily pattern in company local time, e.g. "09:00-14:00, 15:00-18:00"'),
234
+ start_on: z
235
+ .string()
236
+ .optional()
237
+ .describe('Start date YYYY-MM-DD (default: 1st of this month)'),
238
+ end_on: z.string().optional().describe('End date YYYY-MM-DD (default: today)'),
239
+ employee_id: z.string().optional().describe('Employee ID (default: FACTORIAL_EMPLOYEE_ID)'),
240
+ observations: z
241
+ .string()
242
+ .optional()
243
+ .describe('Note stored on every record, e.g. why the hours are being entered now'),
244
+ jitter_minutes: z
245
+ .string()
246
+ .optional()
247
+ .describe('Vary each time by up to this many minutes (default 8, 0 for exact times)'),
248
+ },
249
+ }, async ({ segments, start_on, end_on, employee_id, observations, jitter_minutes }) => {
250
+ const employeeId = resolveTargetEmployeeId(parseOptionalInt(employee_id, 'employee_id'));
251
+ const name = await resolveEmployeeName(employeeId);
252
+ const window = resolveWindow(start_on, end_on, today());
253
+ const pattern = parseSegmentsArg(segments);
254
+ const jitter = parseOptionalInt(jitter_minutes, 'jitter_minutes') ?? 8;
255
+ const note = observations && observations.trim() !== ''
256
+ ? observations.trim()
257
+ : `Entered with the daily pattern on ${today()}`;
258
+ const gapsReport = await preRead('the gaps', async () => {
259
+ const { gaps, coverage } = await findGaps(employeeId, window.start_on, window.end_on);
260
+ return formatGaps({
261
+ employee: { id: employeeId, name },
262
+ startOn: window.start_on,
263
+ endOn: window.end_on,
264
+ gaps,
265
+ coverage,
266
+ });
267
+ });
268
+ const rangeCall = {
269
+ action: 'log_range',
270
+ employee_id: employeeId,
271
+ start_on: window.start_on,
272
+ end_on: window.end_on,
273
+ segments: pattern,
274
+ jitter_minutes: jitter,
275
+ observations: note,
276
+ };
277
+ return userMessage([
278
+ `Fill the missing registro horario of ${name} (${employeeId}) from ${window.start_on} to ${window.end_on} with the daily pattern ${pattern.map(s => `${s.clock_in}-${s.clock_out}`).join(' and ')} (${hours(patternMinutes(pattern))} a day).`,
279
+ '',
280
+ 'The gaps have already been read for you:',
281
+ '',
282
+ gapsReport,
283
+ '',
284
+ 'Do this, in order:',
285
+ '1. Read the Data read line. If it names days without contract data that are not before the start of employment, stop and report; do not write.',
286
+ '2. Tell the person which days are short and ask whether any of them were not worked (illness without a leave record, a day off). Exclude such days by narrowing the range or by running the call on the sub-ranges around them. Never invent hours.',
287
+ `3. Preview the write. Nothing is written by this call: ${toolCall(rangeCall)}`,
288
+ '4. Show the person the preview: how many days and records, the hours, and every skipped day with its reason. Ask them to confirm.',
289
+ '5. Only after they confirm, repeat exactly the same call adding the confirmation_token from the preview. The token lasts 15 minutes and matches only that plan. If it expired, run the preview again.',
290
+ '6. If the result says the write stopped part way, re-run the identical call; the planner writes only what is still missing.',
291
+ `7. Verify: ${toolCall({ action: 'audit', employee_id: employeeId, start_on: window.start_on, end_on: window.end_on })} and report what changed. Every workday should read complete.`,
292
+ '',
293
+ 'For days worked outside the pattern (a Saturday, a half day next to half-day leave), write them one by one with action log_days, for example ' +
294
+ toolCall({
295
+ action: 'log_days',
296
+ employee_id: employeeId,
297
+ days: [{ date: window.end_on, segments: pattern.slice(0, 1) }],
298
+ observations: note,
299
+ }) +
300
+ ', with the same preview and confirmation steps.',
301
+ '',
302
+ `Full guide: ${GUIDE_URI}`,
303
+ ].join('\n'));
304
+ });
305
+ server.registerPrompt('attendance_today', {
306
+ title: "Record today's registro horario",
307
+ description: "Enter today's hours for the configured employee if today is a workday with nothing " +
308
+ 'tracked yet. Made for a daily routine (Claude Code /schedule or a cron running claude -p). ' +
309
+ 'Does nothing on weekends, bank holidays, leave, or when a shift is open or today is complete.',
310
+ argsSchema: {
311
+ segments: z
312
+ .string()
313
+ .describe('Daily pattern in company local time, e.g. "09:00-14:00, 15:00-18:00"'),
314
+ employee_id: z.string().optional().describe('Employee ID (default: FACTORIAL_EMPLOYEE_ID)'),
315
+ observations: z.string().optional().describe('Note stored on the records'),
316
+ jitter_minutes: z
317
+ .string()
318
+ .optional()
319
+ .describe('Vary each time by up to this many minutes (default 8, 0 for exact times)'),
320
+ },
321
+ }, async ({ segments, employee_id, observations, jitter_minutes }) => {
322
+ const explicit = parseOptionalInt(employee_id, 'employee_id');
323
+ const employeeId = resolveTargetEmployeeId(explicit);
324
+ const name = await resolveEmployeeName(employeeId);
325
+ const date = today();
326
+ const pattern = parseSegmentsArg(segments);
327
+ const jitter = parseOptionalInt(jitter_minutes, 'jitter_minutes') ?? 8;
328
+ const note = observations && observations.trim() !== '' ? observations.trim() : 'Daily record';
329
+ const todayReport = await preRead("today's ledger", async () => {
330
+ const { ledger, coverage } = await buildLedger(employeeId, date, date);
331
+ const [row] = ledger;
332
+ return (`Today ${date} for ${name} (${employeeId}): status ${row.status}` +
333
+ (row.shifts.length
334
+ ? `, shifts on record ${row.shifts.map(s => `${s.clock_in}-${s.clock_out ?? 'open'}`).join(' ')}`
335
+ : ', no shifts on record') +
336
+ `.\n${ledgerRow(row)}\n` +
337
+ `Data read: contract data for ${coverage.days_with_contract_data} of 1 day, ${coverage.leave_records} leave records, ${coverage.shift_records} shift records.`);
338
+ });
339
+ const daysCall = {
340
+ action: 'log_days',
341
+ employee_id: employeeId,
342
+ days: [{ date, segments: pattern }],
343
+ jitter_minutes: jitter,
344
+ observations: note,
345
+ };
346
+ return userMessage([
347
+ `Record today's registro horario (${date}) for ${name} (${employeeId}) with the pattern ${pattern.map(s => `${s.clock_in}-${s.clock_out}`).join(' and ')} (${hours(patternMinutes(pattern))}), if and only if today is a workday with nothing tracked yet.`,
348
+ '',
349
+ "Today's ledger has already been read for you:",
350
+ '',
351
+ todayReport,
352
+ '',
353
+ 'Decide as follows:',
354
+ `1. ${toolCall({ action: 'status', employee_id: employeeId })}. If the employee is clocked in (an open shift), write nothing and report that a shift is open.`,
355
+ '2. If the status above is complete, weekend, bank_holiday, on_leave, not_workable or future, write nothing and report in one line why.',
356
+ '3. If the status is no_contract_data, write nothing and report that Factorial has no contract data for today.',
357
+ '4. If the status is half_day_leave, write nothing and report it; the worked half needs a person to say which half it was.',
358
+ `5. If the status is missing and no shift is on record, preview the write: ${toolCall(daysCall)}. Then, in this same session, repeat the identical call adding the confirmation_token from the preview. This unattended confirmation is allowed only when the employee is the configured identity (FACTORIAL_EMPLOYEE_ID)${explicit !== undefined ? '; an explicit employee_id was given, so confirm that it is the configured identity before proceeding, and otherwise stop and ask' : ''}.`,
359
+ '6. If the status is missing but a shift is already on record for today, write nothing and report the partial day; a person decides how to complete it.',
360
+ `7. Verify with ${toolCall({ action: 'audit', employee_id: employeeId, start_on: date, end_on: date })} and report in one line what was written or why nothing was.`,
361
+ '',
362
+ `Full guide: ${GUIDE_URI}`,
363
+ ].join('\n'));
364
+ });
365
+ }
366
+ //# sourceMappingURL=attendance.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"attendance.js","sourceRoot":"","sources":["../../src/prompts/attendance.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"attendance.d.ts","sourceRoot":"","sources":["../../src/tools/attendance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAgEzE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QA2bvD"}
1
+ {"version":3,"file":"attendance.d.ts","sourceRoot":"","sources":["../../src/tools/attendance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AAoFzE,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,SAAS,QAicvD"}
@@ -15,7 +15,8 @@ import { textResponse, formatToolError } from '../tool-utils.js';
15
15
  import { formatPaginationInfo } from '../pagination.js';
16
16
  import { listShifts, getShift, createShift, updateShift, deleteShift, listOpenShifts, clockIn, clockOut, formatLocalIso, } from '../api/index.js';
17
17
  import { hhmm, isoWithOffset, LOCATION_TYPES, HALF_DAY_VALUES, SegmentInputSchema, DayInputSchema, } from '../schemas.js';
18
- import { enumerateDates, formatPlanPreview, hours, parseHHMM, planFingerprint, } from '../attendance/planner.js';
18
+ import { enumerateDates, formatCoverage, formatPlanPreview, hours, parseHHMM, planFingerprint, } from '../attendance/planner.js';
19
+ import { formatAudit, formatGaps } from '../attendance/report.js';
19
20
  import { buildLedger, executeBackfill, findGaps, planBackfill, requestWindow, } from '../attendance/backfill.js';
20
21
  import { getConfiguredEmployeeId, resolveEmployeeName, resolveTargetEmployeeId, } from '../attendance/identity.js';
21
22
  import { payloadFingerprint, requireTargetConfirmation } from '../attendance/gate.js';
@@ -23,8 +24,24 @@ const RETRY_NOTE = 'Re-running the identical call is safe against this run: the
23
24
  'and skips whatever overlaps, so it writes only the missing records. This also covers a POST ' +
24
25
  'that timed out after Factorial committed it. It does not protect against another writer ' +
25
26
  'between the read and the writes (another session, a colleague clocking in through the app).';
27
+ /** Every record when there are few; per-month counts when listing them would drown the result */
26
28
  function describeWrites(writes) {
27
- return writes.map(w => ` ${w.date} ${w.clock_in}-${w.clock_out}`).join('\n');
29
+ if (writes.length <= 62) {
30
+ return writes.map(w => ` ${w.date} ${w.clock_in}-${w.clock_out}`).join('\n');
31
+ }
32
+ const byMonth = new Map();
33
+ for (const w of writes) {
34
+ const month = w.date.slice(0, 7);
35
+ const entry = byMonth.get(month) ?? { records: 0, days: new Set(), minutes: 0 };
36
+ entry.records += 1;
37
+ entry.days.add(w.date);
38
+ entry.minutes += parseHHMM(w.clock_out) - parseHHMM(w.clock_in);
39
+ byMonth.set(month, entry);
40
+ }
41
+ return [...byMonth.entries()]
42
+ .sort(([a], [b]) => a.localeCompare(b))
43
+ .map(([month, e]) => ` ${month} ${e.days.size} days, ${e.records} records, ${hours(e.minutes)}`)
44
+ .join('\n');
28
45
  }
29
46
  function shiftLine(shift) {
30
47
  return `${shift.date} ${shift.clock_in ?? '?'}-${shift.clock_out ?? 'open'}`;
@@ -59,7 +76,10 @@ export function registerAttendanceTool(server) {
59
76
  .number()
60
77
  .optional()
61
78
  .describe('Employee ID. Defaults to FACTORIAL_EMPLOYEE_ID when set'),
62
- employee_ids: z.array(z.number()).optional().describe('Employee IDs (list)'),
79
+ employee_ids: z
80
+ .array(z.number())
81
+ .optional()
82
+ .describe('Employee IDs (list). Defaults to FACTORIAL_EMPLOYEE_ID; pass [] for the whole company'),
63
83
  start_on: z.string().optional().describe('Start date YYYY-MM-DD (list, gaps, log_range)'),
64
84
  end_on: z.string().optional().describe('End date YYYY-MM-DD (list, gaps, log_range)'),
65
85
  ids: z.array(z.number()).optional().describe('Shift IDs (list)'),
@@ -108,6 +128,12 @@ export function registerAttendanceTool(server) {
108
128
  .describe('log_range/log_days: vary each written time by up to this many minutes so a month of ' +
109
129
  'entries does not all read 09:00 exactly. Recommended 5 to 10 when reconstructing ' +
110
130
  'approximate hours. Deterministic per record; the preview lists the exact times.'),
131
+ format: z
132
+ .enum(['summary', 'table', 'json'])
133
+ .optional()
134
+ .default('summary')
135
+ .describe('audit: "summary" (default) lists only the days that need attention, "table" lists every ' +
136
+ 'day, "json" returns the full ledger as JSON'),
111
137
  tolerance_minutes: z
112
138
  .number()
113
139
  .int()
@@ -128,8 +154,18 @@ export function registerAttendanceTool(server) {
128
154
  try {
129
155
  switch (args.action) {
130
156
  case 'list': {
157
+ // Like every other action, list defaults to the configured identity.
158
+ // An explicit empty employee_ids asks for the whole company.
159
+ const configured = getConfiguredEmployeeId();
160
+ const employeeIds = args.employee_ids ??
161
+ (args.employee_id ? [args.employee_id] : configured ? [configured] : undefined);
162
+ const scope = employeeIds && employeeIds.length > 0
163
+ ? `for employee${employeeIds.length > 1 ? 's' : ''} ${employeeIds.join(', ')}`
164
+ : configured
165
+ ? 'company-wide (employee_ids: [] was passed)'
166
+ : 'company-wide (no employee filter and FACTORIAL_EMPLOYEE_ID is not set)';
131
167
  const result = await listShifts({
132
- employee_ids: args.employee_ids ?? (args.employee_id ? [args.employee_id] : undefined),
168
+ employee_ids: employeeIds && employeeIds.length > 0 ? employeeIds : undefined,
133
169
  start_on: args.start_on,
134
170
  end_on: args.end_on,
135
171
  ids: args.ids,
@@ -149,7 +185,7 @@ export function registerAttendanceTool(server) {
149
185
  in_source: s.in_source,
150
186
  observations: s.observations,
151
187
  }));
152
- return textResponse(`Found ${result.meta.total} shifts (${formatPaginationInfo(result.meta)}; paging is ` +
188
+ return textResponse(`Found ${result.meta.total} shifts ${scope} (${formatPaginationInfo(result.meta)}; paging is ` +
153
189
  `client-side, the API returned everything in range). Times are HH:MM company local.\n\n` +
154
190
  JSON.stringify(summary, null, 2));
155
191
  }
@@ -303,17 +339,14 @@ export function registerAttendanceTool(server) {
303
339
  }
304
340
  const employeeId = resolveTargetEmployeeId(args.employee_id);
305
341
  const name = await resolveEmployeeName(employeeId);
306
- const gaps = await findGaps(employeeId, args.start_on, args.end_on, args.tolerance_minutes);
307
- if (gaps.length === 0) {
308
- return textResponse(`No gaps for ${name} (${employeeId}) between ${args.start_on} and ${args.end_on}: ` +
309
- 'every workday not on leave has at least the expected minutes tracked.');
310
- }
311
- const rows = gaps.map(g => ` ${g.date} expected ${hours(g.expected_minutes)} tracked ${hours(g.tracked_minutes)} ` +
312
- `missing ${hours(g.missing_minutes)}${g.half_day_leave ? ` (half-day leave: ${g.half_day_leave})` : ''}`);
313
- const total = gaps.reduce((sum, g) => sum + g.missing_minutes, 0);
314
- return textResponse(`${gaps.length} days with missing hours for ${name} (${employeeId}), ${hours(total)} in total:\n\n` +
315
- `${rows.join('\n')}\n\nWeekends, bank holidays and full-day leave are excluded. ` +
316
- 'Use log_range with the same dates and your daily segments to fill them.');
342
+ const { gaps, coverage } = await findGaps(employeeId, args.start_on, args.end_on, args.tolerance_minutes);
343
+ return textResponse(formatGaps({
344
+ employee: { id: employeeId, name },
345
+ startOn: args.start_on,
346
+ endOn: args.end_on,
347
+ gaps,
348
+ coverage,
349
+ }));
317
350
  }
318
351
  case 'audit': {
319
352
  if (!args.start_on || !args.end_on) {
@@ -321,28 +354,16 @@ export function registerAttendanceTool(server) {
321
354
  }
322
355
  const employeeId = resolveTargetEmployeeId(args.employee_id);
323
356
  const name = await resolveEmployeeName(employeeId);
324
- const ledger = await buildLedger(employeeId, args.start_on, args.end_on, args.tolerance_minutes);
325
- const counts = new Map();
326
- for (const day of ledger)
327
- counts.set(day.status, (counts.get(day.status) ?? 0) + 1);
328
- const expected = ledger.reduce((sum, d) => sum + d.expected_minutes, 0);
329
- const tracked = ledger.reduce((sum, d) => sum + d.tracked_minutes, 0);
330
- const rows = ledger.map(d => {
331
- const shifts = d.shifts.length
332
- ? d.shifts.map(sh => `${sh.clock_in}-${sh.clock_out ?? 'open'}`).join(' ')
333
- : '-';
334
- const leave = d.leave ? ` leave:${d.leave}` : '';
335
- return ` ${d.date} ${(d.day_type ?? 'unknown').padEnd(12)} ${d.status.padEnd(14)} expected ${hours(d.expected_minutes).padEnd(6)} tracked ${hours(d.tracked_minutes).padEnd(6)} ${shifts}${leave}`;
336
- });
337
- const summary = [...counts.entries()].map(([k, v]) => `${v} ${k}`).join(', ');
338
- return textResponse(`Attendance audit for ${name} (${employeeId}), ${args.start_on} to ${args.end_on}\n` +
339
- `Expected ${hours(expected)} across the range, tracked ${hours(tracked)}; days: ${summary}.\n` +
340
- 'Expected comes from the contract pattern; day type and bank holidays from the company ' +
341
- 'calendar in Factorial; leave from approved timeoff records. Times are HH:MM company local. ' +
342
- `A day within ${args.tolerance_minutes} minutes of its expected total counts as complete.\n\n` +
343
- `${rows.join('\n')}\n\n` +
344
- 'Machine-readable ledger:\n' +
345
- JSON.stringify(ledger));
357
+ const { ledger, coverage } = await buildLedger(employeeId, args.start_on, args.end_on, args.tolerance_minutes);
358
+ return textResponse(formatAudit({
359
+ employee: { id: employeeId, name },
360
+ startOn: args.start_on,
361
+ endOn: args.end_on,
362
+ ledger,
363
+ coverage,
364
+ toleranceMinutes: args.tolerance_minutes,
365
+ format: args.format,
366
+ }));
346
367
  }
347
368
  case 'log_range':
348
369
  case 'log_days': {
@@ -378,8 +399,8 @@ export function registerAttendanceTool(server) {
378
399
  };
379
400
  }
380
401
  const window = requestWindow(request);
381
- const { plan } = await planBackfill(request, window.start, window.end);
382
- const preview = formatPlanPreview(plan, { id: employeeId, name }, window, request, args.observations);
402
+ const { plan, facts } = await planBackfill(request, window.start, window.end);
403
+ const preview = formatPlanPreview(plan, { id: employeeId, name }, window, request, args.observations) + (facts.coverage ? `\n\n${formatCoverage(facts.coverage)}` : '');
383
404
  if (plan.writes.length === 0) {
384
405
  return textResponse(`${preview}\n\nNothing to write.`);
385
406
  }