diff --git a/gemini-extension.json b/gemini-extension.json index 78fc2a3..4c5bef0 100644 --- a/gemini-extension.json +++ b/gemini-extension.json @@ -1,7 +1,7 @@ { "name": "brass-monkey", - "version": "1.3.2", - "description": "Secure, intelligent bridge between AI agents and Odoo instances.", + "version": "1.3.3", + "description": "A high-fidelity Gemini CLI extension and MCP bridge for Odoo ERP/CRM.", "main": "dist/index.js", "types": "dist/index.d.ts", "author": "Matthew Martella", diff --git a/package.json b/package.json index 812f4d7..f00961b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "brass-monkey", - "version": "1.3.2", + "version": "1.3.3", "type": "module", "main": "dist/index.js", "scripts": { @@ -13,14 +13,18 @@ "keywords": [ "odoo", "gemini", + "gemini-cli", + "cli", "extension", + "mcp", + "ai-agent", "xml-rpc", "erp", "crm" ], - "author": "", + "author": "Actinon", "license": "MIT", - "description": "Secure, intelligent bridge between AI agents and Odoo instances.", + "description": "A high-fidelity Gemini CLI extension and MCP bridge for Odoo ERP/CRM.", "dependencies": { "@modelcontextprotocol/sdk": "^1.29.0", "keytar": "^7.9.0", diff --git a/skills/odoo-crm/SKILL.md b/skills/odoo-crm/SKILL.md index d026a8d..e65ec37 100644 --- a/skills/odoo-crm/SKILL.md +++ b/skills/odoo-crm/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-crm -description: High-level functional expertise in Odoo's CRM engine, covering Lead qualification, Opportunity management, and Activity tracking. +description: MANDATORY for Leads and Opportunities (crm.lead). Expertise in Odoo's CRM pipeline, stages, and activity tracking. --- # Skill: Odoo CRM & Pipeline Management diff --git a/skills/odoo-data-ops/SKILL.md b/skills/odoo-data-ops/SKILL.md index 68b970b..c8946a9 100644 --- a/skills/odoo-data-ops/SKILL.md +++ b/skills/odoo-data-ops/SKILL.md @@ -29,7 +29,13 @@ Every CRUD tool supports an `instance_alias` parameter. - **Field Categorization:** By default, `search_read` only returns "Base" fields to save context. If you need more, use `include_extended: true` (for extra modules) or `include_computed: true` (for calculated fields). - **Aggregations:** Use `aggregate_records` for BI-style queries (grouping, counting, summing). This is much more context-efficient than reading thousands of records to perform local math. -### 5. Agent-Driven Undo Workflow +### 5. Schema Strictness & Error Recovery +Odoo v18+ is strict about field lists in `search_read`. +- **The Trigger:** If you receive a `ValueError` or `KeyError` stating a field does not exist. +- **The Mandate:** You must STOP and call `inspect_model` to verify the current live schema. Do NOT guess field names. +- **Action:** After finding the correct field, retry the operation with the updated field list. + +### 6. Agent-Driven Undo Workflow If you make a mistake or are asked to "undo" a change: 1. **Locate:** Use `search_read` on the `mail.message` model for the target record to find the "Before Snapshot" you previously posted. 2. **Analyze:** Verify the current record state. Do not attempt a rollback if it violates Odoo's business logic (e.g., trying to revert an invoice that has since been paid). diff --git a/skills/odoo-dev/SKILL.md b/skills/odoo-dev/SKILL.md index bd9afcd..5e7db30 100644 --- a/skills/odoo-dev/SKILL.md +++ b/skills/odoo-dev/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-dev -description: High-level technical expertise in Odoo's customization engine, covering Server Actions, Automated Rules, and Odoo Studio modifications. +description: MANDATORY for customizations. High-level technical expertise in Odoo's customization engine, covering Server Actions, Automated Rules, and Odoo Studio modifications. --- # Skill: Odoo Development & Customization @@ -21,6 +21,7 @@ Before troubleshooting or proposing changes, the agent must identify existing lo ### 2. Server Actions (`ir.actions.server`) - **Purpose:** Execute Python code, update records, or trigger multi-step workflows via the UI. - **Mandate:** When writing Python code in a Server Action, ensure all variables (`env`, `model`, `record`, `records`, `time`) are used correctly. Avoid long-running loops or operations that could cause database locks. +- **HTML Communication:** If using `message_post` within a Server Action to send HTML, remember to pass `body_is_html=True` to prevent escaping. ### 3. Automated Rules (`base.automation`) - **Purpose:** Automatically trigger Server Actions based on specific events. diff --git a/skills/odoo-finance/SKILL.md b/skills/odoo-finance/SKILL.md index 002aae1..4e57ec5 100644 --- a/skills/odoo-finance/SKILL.md +++ b/skills/odoo-finance/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-finance -description: High-level business expertise in Odoo's unified accounting engine, covering Invoicing, Billing, Banking, Consolidation, and Analytics. +description: MANDATORY for Customer Invoices (out_invoice), Vendor Bills (in_invoice), and Payments (account.payment). High-level expertise in Odoo's unified ledger and banking. --- # Skill: Odoo Finance & Accounting diff --git a/skills/odoo-get-started/SKILL.md b/skills/odoo-get-started/SKILL.md index c25663e..9d6d9cc 100644 --- a/skills/odoo-get-started/SKILL.md +++ b/skills/odoo-get-started/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-get-started -description: Orientation skill to establish context immediately upon connecting to an Odoo instance. +description: MANDATORY FIRST STEP. Orientation skill to establish the Odoo World Map context (Version, Apps, Company, Permissions) BEFORE executing any other tools. --- # Skill: Odoo Orientation (The Mandatory Start) diff --git a/skills/odoo-helpdesk/SKILL.md b/skills/odoo-helpdesk/SKILL.md index 8629f1c..c5f45c4 100644 --- a/skills/odoo-helpdesk/SKILL.md +++ b/skills/odoo-helpdesk/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-helpdesk -description: High-level functional expertise in Odoo's Customer Support engine, covering Ticket management, SLA compliance, and multi-channel integration. +description: MANDATORY for Tickets (helpdesk.ticket) and SLAs (helpdesk.sla). Expertise in Odoo's support engine and customer service. --- # Skill: Odoo Helpdesk & Customer Support @@ -24,8 +24,8 @@ Helpdesk is the bridge between a customer problem and a technical solution: - **Sales/Invoicing:** Reference the original `sale.order_id` to verify warranty status or bill for support time via `sale_line_id`. ### 4. Communication Protocol -- **Public Reply:** Use `message_post` with `message_type: 'comment'` for messages sent directly to the customer via email/portal. -- **Internal Note:** Use `message_post` with `subtype_xmlid: 'mail.mt_note'` for internal technical updates. +- **Public Reply:** Use `message_post` with `message_type: 'comment'` and `body_is_html: true` (if sending HTML) for messages sent directly to the customer. +- **Internal Note:** Use `message_post` with `subtype_xmlid: 'mail.mt_note'` and `body_is_html: true` (if sending HTML) for internal technical updates. - **Mandate:** Clearly distinguish between the two. Never leak internal technical jargon to the customer. ## Available Resources diff --git a/skills/odoo-hr/SKILL.md b/skills/odoo-hr/SKILL.md index 77c81a5..832605a 100644 --- a/skills/odoo-hr/SKILL.md +++ b/skills/odoo-hr/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-hr -description: Functional expertise in Odoo's HR module, covering Employee management, Departmental structures, and Employment contracts. +description: MANDATORY for Employees (hr.employee) and Departments (hr.department). Expertise in Odoo's human resources and org structure. --- # Skill: Odoo Human Resources (HR) diff --git a/skills/odoo-introspector/SKILL.md b/skills/odoo-introspector/SKILL.md index e431687..2790afd 100644 --- a/skills/odoo-introspector/SKILL.md +++ b/skills/odoo-introspector/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-introspector -description: Expertise to examine, understand, and interpret Odoo's internal ORM structures and metadata. +description: MANDATORY for technical discovery. Expertise in Odoo's internal ORM structures, field types, and metadata interpretation. --- # Skill: Odoo Introspector diff --git a/skills/odoo-inventory/SKILL.md b/skills/odoo-inventory/SKILL.md index 3764aa3..8a63128 100644 --- a/skills/odoo-inventory/SKILL.md +++ b/skills/odoo-inventory/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-inventory -description: Expertise required to manage Odoo's inventory operations, covering transfers, stock moves, and physical warehouse structure. +description: MANDATORY for Transfers (stock.picking), Stock Moves (stock.move), and Locations (stock.location). Expertise in Odoo's logistics and warehouse structure. --- # Skill: Odoo Inventory & Logistics diff --git a/skills/odoo-mrp/SKILL.md b/skills/odoo-mrp/SKILL.md index 54f8977..5a1df48 100644 --- a/skills/odoo-mrp/SKILL.md +++ b/skills/odoo-mrp/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-mrp -description: High-level functional expertise in Odoo's Manufacturing and Product Lifecycle Management engines, covering production planning, execution, and engineering changes. +description: MANDATORY for Manufacturing Orders (mrp.production), BoMs (mrp.bom), and Work Orders (mrp.workorder). Expertise in Odoo's production engine. --- # Skill: Odoo MRP (Manufacturing) & PLM diff --git a/skills/odoo-products/SKILL.md b/skills/odoo-products/SKILL.md index 659498f..0f29cca 100644 --- a/skills/odoo-products/SKILL.md +++ b/skills/odoo-products/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-products -description: Expertise required to manage Odoo's product catalog, covering both general templates and specific product variants. +description: MANDATORY for Product Templates (product.template) and Variants (product.product). Expertise in Odoo's catalog and attribute management. --- # Skill: Odoo Product Management diff --git a/skills/odoo-projects/SKILL.md b/skills/odoo-projects/SKILL.md index 08a72f2..396b921 100644 --- a/skills/odoo-projects/SKILL.md +++ b/skills/odoo-projects/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-projects -description: High-level functional expertise in Odoo's Project Management engine, covering Tasks, Milestones, and integration with Sales and Accounting. +description: MANDATORY for Projects (project.project) and Tasks (project.task). Expertise in Odoo's project management and milestone tracking. --- # Skill: Odoo Projects, Milestones & Timesheets diff --git a/skills/odoo-purchasing/SKILL.md b/skills/odoo-purchasing/SKILL.md index 65f41eb..3dc167e 100644 --- a/skills/odoo-purchasing/SKILL.md +++ b/skills/odoo-purchasing/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-purchasing -description: High-level functional expertise in Odoo's Purchasing engine, covering RFQs, Purchase Orders, and Vendor Price management. +description: MANDATORY for RFQs and Purchase Orders (purchase.order). Expertise in Odoo's procurement engine and vendor pricing. --- # Skill: Odoo Purchasing & Procurement diff --git a/skills/odoo-relations/SKILL.md b/skills/odoo-relations/SKILL.md index 944af36..0aaae09 100644 --- a/skills/odoo-relations/SKILL.md +++ b/skills/odoo-relations/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-relations -description: Expertise required to manage the res.partner ecosystem, the foundational directory for all people and organizations in Odoo. +description: MANDATORY for Contacts, Customers, and Vendors (res.partner). Expertise in Odoo's central directory and relationship hierarchies. --- # Skill: Odoo Relations (Partners & Contacts) diff --git a/skills/odoo-sales/SKILL.md b/skills/odoo-sales/SKILL.md index 4814415..f5be94f 100644 --- a/skills/odoo-sales/SKILL.md +++ b/skills/odoo-sales/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-sales -description: Functional expertise in Odoo's Sales engine, covering the entire lifecycle from Quotation to Order Fulfillment. +description: MANDATORY for Sales Orders (sale.order), Quotations, and Order Fulfillment. Functional expertise in Odoo's Sales engine and lifecycle states. --- # Skill: Odoo Sales & Order Fulfillment diff --git a/skills/odoo-ux/SKILL.md b/skills/odoo-ux/SKILL.md index 0866d28..4e86777 100644 --- a/skills/odoo-ux/SKILL.md +++ b/skills/odoo-ux/SKILL.md @@ -1,6 +1,6 @@ --- name: odoo-ux -description: Expertise to navigate Odoo's user interface architecture and interpret frontend metadata. +description: MANDATORY for UI navigation. Expertise in Odoo's interface architecture (Menus, Actions, Views) and frontend metadata. --- # Skill: Odoo UX & Navigation diff --git a/src/mcp-server.ts b/src/mcp-server.ts index f3b4aab..12cf5d8 100644 --- a/src/mcp-server.ts +++ b/src/mcp-server.ts @@ -19,7 +19,7 @@ import { fileURLToPath } from 'url'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); // Read package.json for metadata -let version = "1.3.2"; +let version = "1.3.3"; try { const pkgPath = path.resolve(__dirname, "../package.json"); const pkg = JSON.parse(fs.readFileSync(pkgPath, "utf-8")); diff --git a/src/services/audit-service.ts b/src/services/audit-service.ts index bd3368b..a874389 100644 --- a/src/services/audit-service.ts +++ b/src/services/audit-service.ts @@ -81,6 +81,7 @@ export class AuditService { body: `
🤖 AI Agent Action:
${body}
`, message_type: 'comment', subtype_xmlid: 'mail.mt_note', + body_is_html: true, }); } catch (error) { // Odoo models without 'mail.thread' inheritance will fail here. diff --git a/src/services/odoo-client.ts b/src/services/odoo-client.ts index 9507b6c..924e358 100644 --- a/src/services/odoo-client.ts +++ b/src/services/odoo-client.ts @@ -10,6 +10,7 @@ export class OdooClient { private objectClient: any; private uid: number | null = null; private versionInfo: any = null; + private companyIds: number[] = []; constructor(private config: OdooConfig) { const commonUrl = new URL('/xmlrpc/2/common', config.url).toString(); @@ -62,7 +63,18 @@ export class OdooClient { } this.uid = uid as number; - resolve(this.uid); + + // Fetch allowed companies for cross-company visibility + this.objectClient.methodCall( + 'execute_kw', + [db, this.uid, api_key, 'res.users', 'read', [[this.uid]], { fields: ['company_ids'] }], + (companyError: any, userRecords: any) => { + if (!companyError && userRecords && userRecords.length > 0) { + this.companyIds = userRecords[0].company_ids || []; + } + resolve(this.uid as number); + } + ); } ); } @@ -120,6 +132,22 @@ export class OdooClient { const { db, api_key } = this.config; + // Safety Interceptor: Auto-detect HTML in message_post calls + if (method === 'message_post' && kwargs && typeof kwargs.body === 'string') { + const containsHtml = /<[a-z][\s\S]*>/i.test(kwargs.body); + if (containsHtml && kwargs.body_is_html === undefined) { + kwargs.body_is_html = true; + } + } + + // Context Injection: Enable cross-company visibility by default + if (this.companyIds.length > 0) { + kwargs.context = kwargs.context || {}; + if (kwargs.context.allowed_company_ids === undefined) { + kwargs.context.allowed_company_ids = this.companyIds; + } + } + return new Promise((resolve, reject) => { this.objectClient.methodCall( 'execute_kw', @@ -145,7 +173,9 @@ export class OdooClient { /odoo\.exceptions\.UserError: (.*)/, /odoo\.exceptions\.ValidationError: (.*)/, /odoo\.exceptions\.AccessError: (.*)/, - /odoo\.exceptions\.MissingError: (.*)/ + /odoo\.exceptions\.MissingError: (.*)/, + /ValueError: (.*)/, + /KeyError: (.*)/ ]; for (const pattern of businessErrors) { diff --git a/tests/odoo-client.test.ts b/tests/odoo-client.test.ts index 1fa1890..121d809 100644 --- a/tests/odoo-client.test.ts +++ b/tests/odoo-client.test.ts @@ -27,6 +27,7 @@ describe('OdooClient', () => { it('should authenticate successfully', async () => { const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; commonClient.methodCall .mockImplementationOnce((method, params, callback) => { @@ -36,6 +37,10 @@ describe('OdooClient', () => { callback(null, 1); // UID response }); + objectClient.methodCall.mockImplementationOnce((method, params, callback) => { + callback(null, [{ company_ids: [1] }]); // res.users read + }); + const uid = await client.authenticate(); expect(uid).toBe(1); expect(client.majorVersion).toBe(15); @@ -45,6 +50,11 @@ describe('OdooClient', () => { ['test-db', 'admin', 'password', {}], expect.any(Function) ); + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.users', 'read', [[1]], { fields: ['company_ids'] }], + expect.any(Function) + ); }); it('should throw error on authentication failure', async () => { @@ -85,4 +95,118 @@ describe('OdooClient', () => { expect.any(Function) ); }); + + it('should automatically inject body_is_html for message_post with HTML', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + objectClient.methodCall.mockImplementation((method, params, callback) => callback(null, true)); + + const htmlBody = '
Test
'; + await client.executeKw('res.partner', 'message_post', [1], { body: htmlBody }); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'message_post', [1], { body: htmlBody, body_is_html: true }], + expect.any(Function) + ); + }); + + it('should NOT inject body_is_html for plain text message_post', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + objectClient.methodCall.mockImplementation((method, params, callback) => callback(null, true)); + + const plainBody = 'Just a plain text message'; + await client.executeKw('res.partner', 'message_post', [1], { body: plainBody }); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'message_post', [1], { body: plainBody }], + expect.any(Function) + ); + }); + + it('should inject allowed_company_ids into context if companyIds are available', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + // Mock the user read to return company IDs + objectClient.methodCall.mockImplementationOnce((method, params, callback) => { + callback(null, [{ company_ids: [1, 26] }]); + }); + + await client.authenticate(); + + // Now call executeKw and check context + objectClient.methodCall.mockImplementationOnce((method, params, callback) => callback(null, [])); + await client.executeKw('res.partner', 'search', [[]]); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'search', [[]], { context: { allowed_company_ids: [1, 26] } }], + expect.any(Function) + ); + }); + + it('should preserve existing context when injecting allowed_company_ids', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + objectClient.methodCall.mockImplementationOnce((method, params, callback) => { + callback(null, [{ company_ids: [1, 26] }]); + }); + + await client.authenticate(); + + objectClient.methodCall.mockImplementationOnce((method, params, callback) => callback(null, [])); + await client.executeKw('res.partner', 'search', [[]], { context: { lang: 'en_US' } }); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'search', [[]], { context: { lang: 'en_US', allowed_company_ids: [1, 26] } }], + expect.any(Function) + ); + }); + + it('should NOT overwrite allowed_company_ids if explicitly provided', async () => { + const commonClient = (xmlrpc.createSecureClient as any).mock.results[0].value; + const objectClient = (xmlrpc.createSecureClient as any).mock.results[1].value; + + commonClient.methodCall + .mockImplementationOnce((method, params, callback) => callback(null, { server_version: '15.0' })) + .mockImplementationOnce((method, params, callback) => callback(null, 1)); + + objectClient.methodCall.mockImplementationOnce((method, params, callback) => { + callback(null, [{ company_ids: [1, 26] }]); + }); + + await client.authenticate(); + + objectClient.methodCall.mockImplementationOnce((method, params, callback) => callback(null, [])); + await client.executeKw('res.partner', 'search', [[]], { context: { allowed_company_ids: [26] } }); + + expect(objectClient.methodCall).toHaveBeenCalledWith( + 'execute_kw', + ['test-db', 1, 'password', 'res.partner', 'search', [[]], { context: { allowed_company_ids: [26] } }], + expect.any(Function) + ); + }); });