Add/ai native supp (#385)
* feat(branding): implement dynamic branding in service worker and reports * feat(auth): enhance API key scopes and add bookkeeping write scope - Updated transaction write scope description to include additional tools. - Enhanced reports read scope description to reflect new functionality. - Introduced bookkeeping write scope with relevant description. - Updated SCOPE_GROUPS to include bookkeeping domain. - Modified TOOL_SCOPE_MAP to include new bookkeeping operations. - Updated validateApiKey function to return api_key_id and api_key_name for better actor attribution. feat(tests): add unit tests for MCP resource registry - Created tests for data resources to ensure all required fields are present. - Added tests for resource query parsing and retrieval. feat(resources): implement MCP resources for company and accounting data - Added capabilities resource to expose API key capabilities based on granted scopes. - Implemented chart of accounts resource to retrieve active BAS chart. - Created company current resource to fetch active company details. - Developed active fiscal period resource to check posting eligibility. - Implemented recent activity resource to fetch latest journal entries, invoices, and transactions. - Added VAT treatments resource to provide available VAT rates per customer type. feat(pending-operations): introduce risk tiers for operations - Added risk level classification for pending operations to determine auto-commit eligibility. - Implemented functions to classify operation risk levels and identify high-risk operations. feat(migrations): add actor model and risk tier to pending operations - Updated pending_operations table to include actor type and risk level columns. - Enhanced audit_log to mirror actor information for compliance. - Modified validate_and_increment_api_key function to return actor details. - Expanded operation types in pending_operations to include new high-risk operations. * feat: add auto-commit functionality for low-risk pending operations - Implemented shouldAutoCommit function to determine eligibility for auto-commit based on operation type, actor type, and company settings. - Created commitPendingOperation function to handle execution of pending operations with consistent status updates. - Added tests for shouldAutoCommit to cover various scenarios including high-risk operations, user actors, company opt-in status, and monetary thresholds. - Introduced new columns in company_settings for agent_auto_commit_enabled and agent_auto_commit_max_amount to allow companies to opt-in for auto-commit functionality. - Added SQL migration to update the database schema for new auto-commit settings. * feat(idempotency): implement idempotency key handling for safe retries and cleanup * feat: expand API key scopes and pending operations for bookkeeping - Added 'suppliers:write' scope to API key scopes for supplier invoice management. - Updated SCOPE_GROUPS to include the new 'suppliers:write' scope. - Introduced new pending operation types for bookkeeping: close_period, lock_period, run_year_end, set_opening_balances, run_currency_revaluation, explain_voucher_gap, uncategorize_transaction, approve_supplier_invoice, credit_supplier_invoice, and convert_invoice. - Implemented corresponding commit functions for the new operations in the pending operations module. - Enhanced PendingOperation type to include actor model and risk level attributes. - Added tests for new functionality, ensuring proper behavior and constraints in the database. * feat: implement unlockPeriod functionality and related tests * feat: add agent auto-commit settings and related functionality * feat: add attention resource with comprehensive summary of outstanding tasks * feat: enhance pending operations with 'committing' status and immutability checks, improve idempotency handling, and add original voucher reference for credit notes
This commit is contained in:
@@ -0,0 +1,183 @@
|
||||
/**
|
||||
* Structured error shape designed for agents (MCP, automation) that need to
|
||||
* dispatch on error programmatically rather than read the Swedish prose.
|
||||
*
|
||||
* Key design decisions:
|
||||
* - code is machine-readable and stable; agents pattern-match on it
|
||||
* - message_sv is the existing UI string from getErrorMessage()
|
||||
* - message_en gives the agent a translation it can act on without parsing
|
||||
* Swedish tokens
|
||||
* - remediation, when present, points the agent at a tool/args/resource
|
||||
* that fixes the problem. Optional — only set when there's a clear
|
||||
* mechanical next step
|
||||
*
|
||||
* Used by the MCP server's tool error wrapper. UI callers continue to use the
|
||||
* string-only getErrorMessage() — this is additive.
|
||||
*/
|
||||
import { getErrorMessage } from './get-error-message'
|
||||
|
||||
export interface StructuredErrorRemediation {
|
||||
description: string
|
||||
tool?: string
|
||||
args?: Record<string, unknown>
|
||||
resource?: string
|
||||
}
|
||||
|
||||
export interface StructuredError {
|
||||
code: string
|
||||
message_sv: string
|
||||
message_en: string
|
||||
remediation?: StructuredErrorRemediation
|
||||
}
|
||||
|
||||
interface StructuredErrorOptions {
|
||||
/**
|
||||
* Optional: scope the agent attempted to use, for INSUFFICIENT_SCOPE remediation.
|
||||
*/
|
||||
attemptedScope?: string
|
||||
/**
|
||||
* Optional: tool name being called, used in fallback remediation hints.
|
||||
*/
|
||||
toolName?: string
|
||||
}
|
||||
|
||||
const ERROR_CODE_REMEDIATION: Record<string, StructuredErrorRemediation> = {
|
||||
ACCOUNTS_NOT_IN_CHART: {
|
||||
description: 'One or more BAS accounts referenced are not active in the chart of accounts. Activate them via the bookkeeping settings, or use a different category.',
|
||||
resource: 'gnubok://chart-of-accounts',
|
||||
},
|
||||
JOURNAL_ENTRY_NOT_BALANCED: {
|
||||
description: 'Debits and credits do not match. Recalculate the lines so totals are equal before retrying.',
|
||||
},
|
||||
FISCAL_PERIOD_NOT_FOUND: {
|
||||
description: 'No fiscal period covers the entry date. Create or extend the relevant period before retrying.',
|
||||
resource: 'gnubok://period/active',
|
||||
},
|
||||
ENTRY_DATE_OUTSIDE_FISCAL_PERIOD: {
|
||||
description: 'The entry date is outside the active fiscal period. Use a date inside an open period or create one that covers it.',
|
||||
resource: 'gnubok://period/active',
|
||||
},
|
||||
CANNOT_REVERSE_NON_POSTED: {
|
||||
description: 'Only posted entries can be reversed. Commit the draft first or pick a posted entry.',
|
||||
},
|
||||
CANNOT_CORRECT_NON_POSTED: {
|
||||
description: 'Only posted entries can be corrected. Commit the draft first or pick a posted entry.',
|
||||
},
|
||||
ENTRY_ALREADY_REVERSED: {
|
||||
description: 'Another caller reversed this entry concurrently. Re-fetch the entry list and pick a different one.',
|
||||
},
|
||||
PERIOD_NOT_LOCKED: {
|
||||
description: 'The period must be locked before it can be closed. Call gnubok_lock_period first.',
|
||||
tool: 'gnubok_lock_period',
|
||||
},
|
||||
PERIOD_HAS_UNBOOKED_TRANSACTIONS: {
|
||||
description: 'The period contains uncategorized business transactions. Categorize or mark them private before locking.',
|
||||
tool: 'gnubok_list_uncategorized_transactions',
|
||||
},
|
||||
YEAR_END_NOT_RUN: {
|
||||
description: 'Year-end closing must be executed before the period can be closed. Run the year-end procedure first.',
|
||||
},
|
||||
INSUFFICIENT_SCOPE: {
|
||||
description: 'The current API key does not have the required scope. Mint a new key with the missing scope or grant it through the API key settings.',
|
||||
resource: 'gnubok://capabilities',
|
||||
},
|
||||
TRANSACTION_ALREADY_CATEGORIZED: {
|
||||
description: 'The transaction already has a journal entry. Use gnubok_uncategorize_transaction first if you need to recategorize.',
|
||||
tool: 'gnubok_uncategorize_transaction',
|
||||
},
|
||||
INVOICE_ALREADY_SENT: {
|
||||
description: 'The invoice is already sent or paid; sending again would create a duplicate.',
|
||||
},
|
||||
IDEMPOTENCY_KEY_REUSE: {
|
||||
description: 'This idempotency_key was previously used with a different request body. Use a fresh UUID for a new operation, or send the original request body to replay.',
|
||||
},
|
||||
}
|
||||
|
||||
/**
|
||||
* Pull a stable code out of various error shapes.
|
||||
*/
|
||||
function extractCode(error: unknown): string | null {
|
||||
if (typeof error !== 'object' || error === null) return null
|
||||
|
||||
const obj = error as Record<string, unknown>
|
||||
|
||||
// Typed bookkeeping error: { code: 'JOURNAL_ENTRY_NOT_BALANCED', ... }
|
||||
if (typeof obj.code === 'string' && /^[A-Z_]+$/.test(obj.code)) {
|
||||
return obj.code
|
||||
}
|
||||
|
||||
// Wrapped error: { error: { code: '...' } }
|
||||
if (typeof obj.error === 'object' && obj.error !== null) {
|
||||
const inner = obj.error as Record<string, unknown>
|
||||
if (typeof inner.code === 'string' && /^[A-Z_]+$/.test(inner.code)) {
|
||||
return inner.code
|
||||
}
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Heuristically infer a code from the message text when nothing structured
|
||||
* is available. Keeps known-error patterns programmatically dispatchable.
|
||||
*/
|
||||
function inferCode(message: string): string | null {
|
||||
if (/Period must be locked before closing/i.test(message)) return 'PERIOD_NOT_LOCKED'
|
||||
if (/Year-end closing must be executed/i.test(message)) return 'YEAR_END_NOT_RUN'
|
||||
if (/Kan inte låsa period:.*affärstransaktion/i.test(message)) return 'PERIOD_HAS_UNBOOKED_TRANSACTIONS'
|
||||
if (/Insufficient scope/i.test(message)) return 'INSUFFICIENT_SCOPE'
|
||||
if (/already has a journal entry/i.test(message)) return 'TRANSACTION_ALREADY_CATEGORIZED'
|
||||
if (/already been sent/i.test(message) || /already sent/i.test(message)) return 'INVOICE_ALREADY_SENT'
|
||||
if (/locked\/closed fiscal period/i.test(message)) return 'PERIOD_LOCKED'
|
||||
if (/Bokföringen är låst/i.test(message)) return 'PERIOD_LOCKED'
|
||||
if (/Transaction not found/i.test(message)) return 'NOT_FOUND'
|
||||
if (/Invoice not found/i.test(message)) return 'NOT_FOUND'
|
||||
return null
|
||||
}
|
||||
|
||||
function extractEnglishMessage(error: unknown): string {
|
||||
if (typeof error === 'string') return error
|
||||
if (error instanceof Error) return error.message
|
||||
if (typeof error === 'object' && error !== null) {
|
||||
const obj = error as Record<string, unknown>
|
||||
if (typeof obj.error === 'string') return obj.error
|
||||
if (typeof obj.message === 'string') return obj.message
|
||||
if (typeof obj.error === 'object' && obj.error !== null) {
|
||||
const inner = obj.error as Record<string, unknown>
|
||||
if (typeof inner.message === 'string') return inner.message
|
||||
}
|
||||
}
|
||||
return 'Unknown error'
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a StructuredError for an arbitrary thrown value.
|
||||
*
|
||||
* Always returns a valid StructuredError; never throws.
|
||||
*/
|
||||
export function getStructuredError(
|
||||
error: unknown,
|
||||
options: StructuredErrorOptions = {}
|
||||
): StructuredError {
|
||||
const message_en = extractEnglishMessage(error)
|
||||
const message_sv = getErrorMessage(error)
|
||||
|
||||
const code = extractCode(error) ?? inferCode(message_en) ?? 'UNKNOWN_ERROR'
|
||||
|
||||
let remediation = ERROR_CODE_REMEDIATION[code]
|
||||
|
||||
// Specialize INSUFFICIENT_SCOPE with the actual scope name when known.
|
||||
if (code === 'INSUFFICIENT_SCOPE' && options.attemptedScope && remediation) {
|
||||
remediation = {
|
||||
...remediation,
|
||||
description: `The current API key does not have the "${options.attemptedScope}" scope. Mint a new key with that scope or add it to the existing key in API settings.`,
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
code,
|
||||
message_sv,
|
||||
message_en,
|
||||
...(remediation ? { remediation } : {}),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user