log_time
Record completed work sessions with task details, duration, and optional date/tags to maintain accurate time tracking records.
Instructions
Log a completed time entry. This tool records work you've done.
Natural language examples Claude should parse:
"2h on security review" → task: "security review", duration: "2h"
"Client meeting yesterday 90 minutes" → task: "Client meeting", duration: "90m", date: "yesterday"
"Just finished 1.5h on code review" → task: "code review", duration: "1.5h"
Claude should extract:
task: What was worked on
duration: How long (2h, 90m, 1.5h, etc.)
time: When (optional, defaults to now)
date: Which day (optional, defaults to today)
tags: Inferred or explicit tags
company: Which company (optional, uses default)
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| company | No | Company to log time for (optional, uses default if omitted) | |
| date | No | Date of work (e.g., "today", "yesterday", "2025-10-17", omit for today) | |
| duration | Yes | Duration (e.g., "2h", "90m", "1.5h") | |
| tags | No | Tags to categorize work (e.g., ["development", "security"]) | |
| task | Yes | Task description (e.g., "Conduit MCP: Security review") | |
| time | No | Time when work was done (e.g., "14:30", "2 hours ago", omit for now) |
Implementation Reference
- src/tools/log-time.ts:76-167 (handler)The core handler function for the 'log_time' tool. It processes LogTimeInput arguments: resolves company, sanitizes task, parses duration/date/time/tags, creates a TimeEntry, saves it to the company's markdown file via MarkdownManager, loads weekly summary, checks parse issues, and constructs a detailed markdown response with totals, commitment progress, and warnings.handler: withErrorHandler('logging time', async (args: LogTimeInput) => { // Resolve company (single-company mode auto-selects, multi-company requires explicit) const userInput = `${args.task || ''} ${args.duration || ''}`.trim(); const company = getCompanyForOperation(args.company, userInput); // Sanitize and validate task description const sanitizedTask = sanitizeTaskDescription(args.task); // Parse inputs const duration = parseDuration(args.duration); const date = parseDate(args.date); const time = parseTime(args.time, date); const tags = args.tags || []; // Create time entry const entry: TimeEntry = { time: formatTime(time), task: sanitizedTask, duration: duration.hours, tags, date: formatDate(date) }; // Save to markdown await markdownManager.addEntry(company, entry); // Load weekly summary for confirmation const { year, week } = getISOWeek(date); const summary = await markdownManager.getWeeklySummary(company, year, week); // Check for parse issues const parseIssues = markdownManager.getParseIssues(); // Build response let response = `✓ Logged ${duration.formatted} for "${sanitizedTask}" at ${entry.time}`; if (args.date && args.date !== 'today') { response += ` on ${entry.date}`; } if (tags.length > 0) { response += ` [${tags.map(t => '#' + t).join(' ')}]`; } response += '\n\n'; if (summary) { response += `**Week ${week} Status:**\n`; response += `• Total: ${summary.totalHours.toFixed(1)}h`; const config = await markdownManager.loadConfig(company); if (config.commitments.total) { const limit = config.commitments.total.limit; const stats = summaryCalculator.getCommitmentStats(summary.totalHours, limit); response += ` / ${limit}h (${stats.percentage}%)`; if (stats.status === 'over') { response += ` ⚠️ OVER LIMIT`; } else if (stats.status === 'approaching') { response += ` ⚠️ Close to limit`; } } response += '\n'; // Show commitment breakdown for (const [commitment, hours] of Object.entries(summary.byCommitment)) { const limit = config.commitments[commitment]?.limit; if (limit) { const stats = summaryCalculator.getCommitmentStats(hours, limit); const warning = stats.status !== 'within' ? ` ${stats.indicator}` : ''; response += `• ${capitalizeName(commitment)}: ${hours.toFixed(1)}h / ${limit}h (${stats.percentage}%)${warning}\n`; } } } // Add parse warnings if any if (parseIssues.warnings.length > 0) { response += '\n\n⚠️ **Parse Warnings:**\n'; for (const warning of parseIssues.warnings) { response += `• ${warning}\n`; } if (parseIssues.unparsedLines.length > 0 && parseIssues.unparsedLines.length <= 3) { response += '\nUnparsed lines:\n'; for (const { lineNumber, content } of parseIssues.unparsedLines) { response += ` Line ${lineNumber}: ${content}\n`; } } } return createTextResponse(response); })
- src/tools/log-time.ts:38-68 (schema)JSON Schema definition for LogTimeInput used by the log_time tool, specifying properties for task, duration (required), time, date, tags, and optional company.inputSchema: { type: 'object', properties: { task: { type: 'string', description: 'Task description (e.g., "Conduit MCP: Security review")' }, duration: { type: 'string', description: 'Duration (e.g., "2h", "90m", "1.5h")' }, time: { type: 'string', description: 'Time when work was done (e.g., "14:30", "2 hours ago", omit for now)' }, date: { type: 'string', description: 'Date of work (e.g., "today", "yesterday", "2025-10-17", omit for today)' }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags to categorize work (e.g., ["development", "security"])' }, company: { type: 'string', description: 'Company to log time for (optional, uses default if omitted)' } }, required: ['task', 'duration'] },
- src/tools/log-time.ts:16-168 (registration)Registration of the 'log_time' tool using registerTool from ./registry.js. Includes name, detailed description with parsing examples, inputSchema, annotations, and handler.registerTool({ name: 'log_time', description: `Log a completed time entry. This tool records work you've done. Natural language examples Claude should parse: - Single company mode: "2h on security review" → task: "security review", duration: "2h" - Multi company mode: "hm 2h on security review" → company: "HeliMods", duration: "2h", task: "security review" - Multi company mode: "2h debugging for stellantis" → duration: "2h", task: "debugging", company: "Stellantis" - "Client meeting yesterday 90 minutes" → task: "Client meeting", duration: "90m", date: "yesterday" Claude should extract: - task: What was worked on - duration: How long (2h, 90m, 1.5h, etc.) - time: When (optional, defaults to now) - date: Which day (optional, defaults to today) - tags: Inferred or explicit tags - company: CRITICAL - In multi-company mode, MUST extract from user input using: * Prefix pattern: "company/abbrev [duration] [task]" (e.g., "hm 2h debugging", "stellantis 1h meeting") * Suffix pattern: "[duration] [task] for company/abbrev" (e.g., "2h debugging for hm", "1h meeting for stellantis") * Full company names work (case-insensitive): "HeliMods", "Stellantis", etc. * Abbreviations work (case-insensitive): "HM", "STLA", etc. * In single-company mode, company is automatic (ignore any company in input)`, inputSchema: { type: 'object', properties: { task: { type: 'string', description: 'Task description (e.g., "Conduit MCP: Security review")' }, duration: { type: 'string', description: 'Duration (e.g., "2h", "90m", "1.5h")' }, time: { type: 'string', description: 'Time when work was done (e.g., "14:30", "2 hours ago", omit for now)' }, date: { type: 'string', description: 'Date of work (e.g., "today", "yesterday", "2025-10-17", omit for today)' }, tags: { type: 'array', items: { type: 'string' }, description: 'Tags to categorize work (e.g., ["development", "security"])' }, company: { type: 'string', description: 'Company to log time for (optional, uses default if omitted)' } }, required: ['task', 'duration'] }, annotations: { title: 'Log Time', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false }, handler: withErrorHandler('logging time', async (args: LogTimeInput) => { // Resolve company (single-company mode auto-selects, multi-company requires explicit) const userInput = `${args.task || ''} ${args.duration || ''}`.trim(); const company = getCompanyForOperation(args.company, userInput); // Sanitize and validate task description const sanitizedTask = sanitizeTaskDescription(args.task); // Parse inputs const duration = parseDuration(args.duration); const date = parseDate(args.date); const time = parseTime(args.time, date); const tags = args.tags || []; // Create time entry const entry: TimeEntry = { time: formatTime(time), task: sanitizedTask, duration: duration.hours, tags, date: formatDate(date) }; // Save to markdown await markdownManager.addEntry(company, entry); // Load weekly summary for confirmation const { year, week } = getISOWeek(date); const summary = await markdownManager.getWeeklySummary(company, year, week); // Check for parse issues const parseIssues = markdownManager.getParseIssues(); // Build response let response = `✓ Logged ${duration.formatted} for "${sanitizedTask}" at ${entry.time}`; if (args.date && args.date !== 'today') { response += ` on ${entry.date}`; } if (tags.length > 0) { response += ` [${tags.map(t => '#' + t).join(' ')}]`; } response += '\n\n'; if (summary) { response += `**Week ${week} Status:**\n`; response += `• Total: ${summary.totalHours.toFixed(1)}h`; const config = await markdownManager.loadConfig(company); if (config.commitments.total) { const limit = config.commitments.total.limit; const stats = summaryCalculator.getCommitmentStats(summary.totalHours, limit); response += ` / ${limit}h (${stats.percentage}%)`; if (stats.status === 'over') { response += ` ⚠️ OVER LIMIT`; } else if (stats.status === 'approaching') { response += ` ⚠️ Close to limit`; } } response += '\n'; // Show commitment breakdown for (const [commitment, hours] of Object.entries(summary.byCommitment)) { const limit = config.commitments[commitment]?.limit; if (limit) { const stats = summaryCalculator.getCommitmentStats(hours, limit); const warning = stats.status !== 'within' ? ` ${stats.indicator}` : ''; response += `• ${capitalizeName(commitment)}: ${hours.toFixed(1)}h / ${limit}h (${stats.percentage}%)${warning}\n`; } } } // Add parse warnings if any if (parseIssues.warnings.length > 0) { response += '\n\n⚠️ **Parse Warnings:**\n'; for (const warning of parseIssues.warnings) { response += `• ${warning}\n`; } if (parseIssues.unparsedLines.length > 0 && parseIssues.unparsedLines.length <= 3) { response += '\nUnparsed lines:\n'; for (const { lineNumber, content } of parseIssues.unparsedLines) { response += ` Line ${lineNumber}: ${content}\n`; } } } return createTextResponse(response); }) });
- src/types/index.ts:62-69 (schema)TypeScript interface definition for LogTimeInput, matching the tool's input schema and used in the handler signature.export interface LogTimeInput { task: string; duration: string; // "2h", "90m", "1.5h", etc. time?: string; // "14:30", "2 hours ago", omit for now date?: string; // "today", "yesterday", "2025-10-17" tags?: string[]; company?: string; }