@bradtech/sales-skills 1.0.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.
@@ -0,0 +1,83 @@
1
+ ---
2
+ name: brevo-integration
3
+ description: >-
4
+ Allows querying and updating your Brevo account, and syncing/enriching contacts between Odoo and Brevo.
5
+ Trigger when the user wants to list contacts in Brevo, create/update a contact on Brevo,
6
+ or synchronize/enrich contact records between Odoo and Brevo.
7
+ ---
8
+
9
+ # Brevo Integration & Odoo Sync Skill
10
+
11
+ ## Presentation
12
+ This Skill provides a CLI tool executed via Bun to interact with Brevo via its official API v3, along with an orchestration workflow to bidirectionally sync and enrich contact details between Odoo CRM and Brevo.
13
+
14
+ ## Prerequisites and Credentials
15
+ The CLI automatically loads parameters from your `.env` file at the root of the sales-skills repository:
16
+ - Expected path: `/Users/crapougnax/CODE/BRAD2026/sales-skills/.env`
17
+
18
+ Please configure your Brevo v3 API key:
19
+ ```env
20
+ BREVO_API_KEY="your_brevo_api_key_v3"
21
+ ```
22
+
23
+ ## Quick Start / Usage
24
+
25
+ All commands must be executed using `bun run`. Always redirect JSON output to `.log/` folders via the `--output` option.
26
+
27
+ ### 1. List Brevo Contacts
28
+ ```bash
29
+ bun run skills/vendor/brevo/cli.ts list-contacts \
30
+ --limit 50 \
31
+ --output ".log/brevo_contacts.json"
32
+ ```
33
+
34
+ ### 2. Retrieve a Brevo Contact by Email
35
+ ```bash
36
+ bun run skills/vendor/brevo/cli.ts get-contact \
37
+ --email "contact@acme.com" \
38
+ --output ".log/brevo_contact_details.json"
39
+ ```
40
+
41
+ ### 3. Create or Update a Brevo Contact
42
+ ```bash
43
+ bun run skills/vendor/brevo/cli.ts create-or-update-contact \
44
+ --email "contact@acme.com" \
45
+ --firstname "Alice" \
46
+ --lastname "Smith" \
47
+ --phone "33600000000" \
48
+ --output ".log/brevo_update_result.json"
49
+ ```
50
+
51
+ ---
52
+
53
+ ## Bidirectional Sync & Enrichment Workflow (Odoo <-> Brevo)
54
+
55
+ When requested to synchronize or enrich contacts between Odoo and Brevo, the agent performs these steps:
56
+
57
+ ### Step 1: Query and Extract Contacts
58
+ Identify target contacts (either by a specific email, email domain, or listing the latest modified records).
59
+ 1. Query Odoo:
60
+ ```bash
61
+ bun run skills/vendor/odoo/cli.ts search-partners \
62
+ --query "<QUERY>" \
63
+ --limit 50 \
64
+ --output ".log/odoo_search.json"
65
+ ```
66
+ 2. Query Brevo using `get-contact` for each target email found, or list contacts.
67
+
68
+ ### Step 2: Data Comparison and Discrepancy Detection
69
+ Compare attributes across Odoo and Brevo:
70
+ - **Missing Contacts**: Exists in Odoo but not in Brevo, or vice-versa.
71
+ - **Incomplete Fields**:
72
+ - Name: Exists on Brevo (`FIRSTNAME`, `LASTNAME`) but Odoo only has the raw email string.
73
+ - Phone: Missing on Odoo but present on Brevo (or vice-versa).
74
+
75
+ ### Step 3: Interactive Sync Proposal
76
+ Present a clean markdown table showing:
77
+ - Missing contacts on both platforms.
78
+ - Gaps in names or phone attributes.
79
+ - **Prompt the user for validation before applying any creation or enrichment.**
80
+
81
+ ### Step 4: Apply Updates
82
+ - **Enrich Brevo**: Call `create-or-update-contact` with Odoo details.
83
+ - **Enrich Odoo**: Call `update-partner` or `create-contact` with Brevo details.
@@ -0,0 +1,167 @@
1
+ import { CliCommand } from '@quatrain/cli';
2
+ import { ApiClient } from '@quatrain/api-client';
3
+ import { Skills } from '@quatrain/skills';
4
+
5
+ class BrevoClient {
6
+ private client: ApiClient;
7
+ private apiKey: string;
8
+
9
+ constructor() {
10
+ this.apiKey = process.env.BREVO_API_KEY || '';
11
+ if (!this.apiKey) {
12
+ Skills.error(
13
+ 'Error: The BREVO_API_KEY environment variable must be defined.\n' +
14
+ 'Please declare it in your local `.env` file.'
15
+ );
16
+ process.exit(1);
17
+ }
18
+
19
+ // Instantiate ApiClient with Brevo base URL
20
+ this.client = new ApiClient('https://api.brevo.com/v3');
21
+ }
22
+
23
+ private getHeaders() {
24
+ return {
25
+ 'accept': 'application/json',
26
+ 'content-type': 'application/json',
27
+ 'api-key': this.apiKey
28
+ };
29
+ }
30
+
31
+ async listContacts(limit = 50, offset = 0) {
32
+ try {
33
+ const response = await this.client.get('contacts', {
34
+ limit,
35
+ offset,
36
+ headers: this.getHeaders()
37
+ });
38
+ return response.data;
39
+ } catch (err: any) {
40
+ Skills.error(`Brevo API error (listContacts): ${err.message}`);
41
+ process.exit(1);
42
+ }
43
+ }
44
+
45
+ async getContact(email: string) {
46
+ try {
47
+ const encodedEmail = encodeURIComponent(email);
48
+ const response = await this.client.get(`contacts/${encodedEmail}`, {
49
+ headers: this.getHeaders()
50
+ });
51
+ return response.data;
52
+ } catch (err: any) {
53
+ if (err.message.includes('404')) {
54
+ return null;
55
+ }
56
+ Skills.error(`Brevo API error (getContact): ${err.message}`);
57
+ process.exit(1);
58
+ }
59
+ }
60
+
61
+ async createOrUpdateContact(
62
+ email: string,
63
+ firstname?: string,
64
+ lastname?: string,
65
+ phone?: string,
66
+ customAttributes?: any
67
+ ) {
68
+ const attributes: any = {};
69
+ if (firstname) attributes.FIRSTNAME = firstname;
70
+ if (lastname) attributes.LASTNAME = lastname;
71
+ if (phone) attributes.SMS = phone;
72
+ if (customAttributes) {
73
+ Object.assign(attributes, customAttributes);
74
+ }
75
+
76
+ const payload: any = {
77
+ email,
78
+ updateEnabled: true
79
+ };
80
+ if (Object.keys(attributes).length > 0) {
81
+ payload.attributes = attributes;
82
+ }
83
+
84
+ try {
85
+ await this.client.post('contacts', payload);
86
+ return { status: 'success', email, action: 'created_or_updated' };
87
+ } catch (err: any) {
88
+ Skills.error(`Brevo API error (createOrUpdateContact): ${err.message}`);
89
+ process.exit(1);
90
+ }
91
+ }
92
+ }
93
+
94
+ async function main() {
95
+ const program = new CliCommand();
96
+ program
97
+ .name('brevo_cli')
98
+ .description('CLI to interact with Brevo API v3');
99
+
100
+ // Command: list-contacts
101
+ program
102
+ .command('list-contacts')
103
+ .description('List Brevo contacts')
104
+ .option('--limit <number>', 'Number of records to retrieve', (val) => parseInt(val, 10), 50)
105
+ .option('--offset <number>', 'Pagination offset', (val) => parseInt(val, 10), 0)
106
+ .requiredOption('--output <path>', 'Output JSON file path')
107
+ .action(async (options) => {
108
+ const client = new BrevoClient();
109
+ const result = await client.listContacts(options.limit, options.offset);
110
+ await Skills.writeOutput(result, options.output);
111
+ });
112
+
113
+ // Command: get-contact
114
+ program
115
+ .command('get-contact')
116
+ .description('Get contact details by email')
117
+ .requiredOption('--email <email>', 'Contact email address')
118
+ .requiredOption('--output <path>', 'Output JSON file path')
119
+ .action(async (options) => {
120
+ const client = new BrevoClient();
121
+ let result = await client.getContact(options.email);
122
+ if (result === null) {
123
+ result = { error: 'contact_not_found', email: options.email };
124
+ }
125
+ await Skills.writeOutput(result, options.output);
126
+ });
127
+
128
+ // Command: create-or-update-contact
129
+ program
130
+ .command('create-or-update-contact')
131
+ .description('Create or update a Brevo contact')
132
+ .requiredOption('--email <email>', 'Contact email address')
133
+ .option('--firstname <name>', 'Contact first name')
134
+ .option('--lastname <name>', 'Contact last name')
135
+ .option('--phone <sms>', 'SMS phone number attribute')
136
+ .option('--attributes <json-string>', 'JSON string representing additional custom attributes')
137
+ .requiredOption('--output <path>', 'Output JSON file path')
138
+ .action(async (options) => {
139
+ const client = new BrevoClient();
140
+ let customAttrs = null;
141
+ if (options.attributes) {
142
+ try {
143
+ customAttrs = JSON.parse(options.attributes);
144
+ } catch (err: any) {
145
+ Skills.error(`Error parsing attributes JSON: ${err.message}`);
146
+ process.exit(1);
147
+ }
148
+ }
149
+ const result = await client.createOrUpdateContact(
150
+ options.email,
151
+ options.firstname,
152
+ options.lastname,
153
+ options.phone,
154
+ customAttrs
155
+ );
156
+ await Skills.writeOutput(result, options.output);
157
+ });
158
+
159
+ await program.parseAsync(process.argv);
160
+ }
161
+
162
+ if (import.meta.main) {
163
+ main().catch((err) => {
164
+ Skills.error(`Unexpected process error: ${err.message}`);
165
+ process.exit(1);
166
+ });
167
+ }
@@ -0,0 +1,66 @@
1
+ ---
2
+ name: google-calendar
3
+ description: >-
4
+ Allows interacting with your Google Calendar to read, create, or delete events.
5
+ Trigger when the user asks to view upcoming meetings, schedule a new meeting, or cancel an event.
6
+ ---
7
+
8
+ # Google Calendar Integration Skill
9
+
10
+ ## Presentation
11
+ This Skill provides a CLI tool executed via Bun to interact with Google Calendar via the official API using a Google Cloud Service Account. It allows managing business meetings, leads follow-ups, and calendar events.
12
+
13
+ ## Prerequisites and Authentication
14
+ The script requires a Google Cloud Service Account key file saved at the root of the sales-skills repository:
15
+ - Expected path: `/Users/crapougnax/CODE/BRAD2026/sales-skills/service_account.json`
16
+
17
+ **Critical Configuration Steps:**
18
+ 1. **Enable Google Calendar API**: Ensure the API is enabled on your GCP Console. You can activate it by visiting [Google Calendar API Console](https://console.developers.google.com/apis/api/calendar-json.googleapis.com/overview).
19
+ 2. **Share Your Calendar**: Share the target calendar with the service account email (ending in `@...iam.gserviceaccount.com`) in the Google Calendar web settings, granting *"Make changes to events"* permissions.
20
+ 3. **External Sharing Restrictions (Google Workspace)**: If options are grayed out, your administrator must allow external calendar management in `admin.google.com` under *Apps > Google Workspace > Calendar > Sharing Settings > External sharing options*.
21
+
22
+ ## Quick Start / Usage
23
+
24
+ All commands must be executed using `bun run`. Always redirect JSON output to `.log/` folders via the `--output` option.
25
+
26
+ ### 1. List Calendar Events
27
+ ```bash
28
+ bun run skills/vendor/google/cli.ts list-events \
29
+ --calendar-id "your_email@gmail.com" \
30
+ --limit 10 \
31
+ --output ".log/events_list.json"
32
+ ```
33
+
34
+ **List events for a specific date range:**
35
+ ```bash
36
+ bun run skills/vendor/google/cli.ts list-events \
37
+ --calendar-id "your_email@gmail.com" \
38
+ --time-min "2026-06-17T00:00:00Z" \
39
+ --time-max "2026-06-17T23:59:59Z" \
40
+ --limit 50 \
41
+ --output ".log/past_events.json"
42
+ ```
43
+
44
+ ### 2. Create a Calendar Event
45
+ ```bash
46
+ bun run skills/vendor/google/cli.ts create-event \
47
+ --calendar-id "your_email@gmail.com" \
48
+ --summary "Technical Review Meeting" \
49
+ --start "2026-06-25 14:00:00" \
50
+ --duration 1.5 \
51
+ --description "Project architecture walkthrough" \
52
+ --output ".log/event_created.json"
53
+ ```
54
+
55
+ ### 3. Delete a Calendar Event
56
+ ```bash
57
+ bun run skills/vendor/google/cli.ts delete-event \
58
+ --calendar-id "your_email@gmail.com" \
59
+ --event-id "calendar_event_id_to_delete" \
60
+ --output ".log/event_deleted.json"
61
+ ```
62
+
63
+ ## Common Issues & Gotchas
64
+ 1. **Unactivated API**: If you receive a "Precondition check failed" or 403 error, verify that the Google Calendar API is activated in GCP console.
65
+ 2. **Missing Permissions**: If you get a 404 or authorization error, confirm the Service Account email is added to the Google Calendar sharing list.
66
+ 3. **Date/Time format**: Ensure the start date uses the format `YYYY-MM-DD HH:MM:SS`.
@@ -0,0 +1,150 @@
1
+ import { google } from 'googleapis';
2
+ import { Skills } from '@quatrain/skills';
3
+ import { existsSync } from 'node:fs';
4
+ import { join } from 'node:path';
5
+
6
+ const SCOPES = ['https://www.googleapis.com/auth/calendar'];
7
+
8
+ function findServiceAccountFile(): string | null {
9
+ if (process.env.GOOGLE_APPLICATION_CREDENTIALS) {
10
+ if (existsSync(process.env.GOOGLE_APPLICATION_CREDENTIALS)) {
11
+ return process.env.GOOGLE_APPLICATION_CREDENTIALS;
12
+ }
13
+ Skills.warn(`Warning: GOOGLE_APPLICATION_CREDENTIALS env var is set to '${process.env.GOOGLE_APPLICATION_CREDENTIALS}', but the file does not exist.`);
14
+ }
15
+
16
+ const currentDir = process.cwd();
17
+ const scriptDir = import.meta.dir;
18
+
19
+ const possiblePaths = [
20
+ join(currentDir, 'service_account.json'),
21
+ join(scriptDir, '..', '..', 'service_account.json'), // skills/vendor/google/../../service_account.json
22
+ join(scriptDir, '..', '..', '..', 'service_account.json'),
23
+ ];
24
+
25
+ for (const path of possiblePaths) {
26
+ if (existsSync(path)) {
27
+ return path;
28
+ }
29
+ }
30
+ return null;
31
+ }
32
+
33
+ export class GoogleCalendarClient {
34
+ private calendar: any;
35
+
36
+ constructor() {
37
+ const keyFile = findServiceAccountFile();
38
+ if (!keyFile) {
39
+ Skills.error(
40
+ "Error: The Google Service Account key credentials file is missing.\n" +
41
+ "Please place 'service_account.json' at the root of your repository, or specify its path " +
42
+ "using the 'GOOGLE_APPLICATION_CREDENTIALS' environment variable."
43
+ );
44
+ process.exit(1);
45
+ }
46
+
47
+ try {
48
+ const auth = new google.auth.GoogleAuth({
49
+ keyFile,
50
+ scopes: SCOPES,
51
+ });
52
+ this.calendar = google.calendar({ version: 'v3', auth });
53
+ } catch (err: any) {
54
+ Skills.error(`Error initializing Google Calendar API: ${err.message}`);
55
+ process.exit(1);
56
+ }
57
+ }
58
+
59
+ async listEvents(calendarId = 'primary', limit = 10, timeMin?: string, timeMax?: string) {
60
+ try {
61
+ const params: any = {
62
+ calendarId,
63
+ maxResults: limit,
64
+ singleEvents: true,
65
+ orderBy: 'startTime',
66
+ };
67
+
68
+ if (timeMin) {
69
+ params.timeMin = timeMin;
70
+ } else {
71
+ params.timeMin = new Date().toISOString();
72
+ }
73
+
74
+ if (timeMax) {
75
+ params.timeMax = timeMax;
76
+ }
77
+
78
+ const response = await this.calendar.events.list(params);
79
+ return response.data.items || [];
80
+ } catch (err: any) {
81
+ Skills.error(`Google Calendar API error (listEvents): ${err.message}`);
82
+ process.exit(1);
83
+ }
84
+ }
85
+
86
+ async createEvent(
87
+ summary: string,
88
+ startTimeStr: string,
89
+ durationHours = 1.0,
90
+ description?: string,
91
+ calendarId = 'primary'
92
+ ) {
93
+ try {
94
+ const normalizedStartStr = startTimeStr.replace(' ', 'T');
95
+ const startDt = new Date(normalizedStartStr);
96
+ if (isNaN(startDt.getTime())) {
97
+ throw new Error(`Invalid start date format: ${startTimeStr}`);
98
+ }
99
+
100
+ const endDt = new Date(startDt.getTime() + durationHours * 60 * 60 * 1000);
101
+ const timeZone = startTimeStr.endsWith('Z') ? 'UTC' : 'Europe/Paris';
102
+
103
+ const eventBody: any = {
104
+ summary,
105
+ start: {
106
+ dateTime: startDt.toISOString(),
107
+ timeZone,
108
+ },
109
+ end: {
110
+ dateTime: endDt.toISOString(),
111
+ timeZone,
112
+ },
113
+ };
114
+
115
+ if (description) {
116
+ eventBody.description = description;
117
+ }
118
+
119
+ const response = await this.calendar.events.insert({
120
+ calendarId,
121
+ requestBody: eventBody,
122
+ });
123
+
124
+ const event = response.data;
125
+ return {
126
+ id: event.id,
127
+ summary: event.summary,
128
+ htmlLink: event.htmlLink,
129
+ start: event.start?.dateTime,
130
+ end: event.end?.dateTime,
131
+ };
132
+ } catch (err: any) {
133
+ Skills.error(`Google Calendar API error (createEvent): ${err.message}`);
134
+ process.exit(1);
135
+ }
136
+ }
137
+
138
+ async deleteEvent(eventId: string, calendarId = 'primary') {
139
+ try {
140
+ await this.calendar.events.delete({
141
+ calendarId,
142
+ eventId,
143
+ });
144
+ return { id: eventId, status: 'deleted' };
145
+ } catch (err: any) {
146
+ Skills.error(`Google Calendar API error (deleteEvent): ${err.message}`);
147
+ process.exit(1);
148
+ }
149
+ }
150
+ }
@@ -0,0 +1,74 @@
1
+ import { CliCommand } from '@quatrain/cli';
2
+ import { Skills } from '@quatrain/skills';
3
+ import { GoogleCalendarClient } from './calendar';
4
+
5
+ async function main() {
6
+ const program = new CliCommand();
7
+ program
8
+ .name('google_cli')
9
+ .description('CLI to interact with Google APIs using Service Account');
10
+
11
+ // Subcommand: list-events
12
+ program
13
+ .command('list-events')
14
+ .description('List upcoming calendar events')
15
+ .option('--calendar-id <id>', 'Calendar ID (email or primary)', 'primary')
16
+ .requiredOption('--limit <number>', 'Maximum number of events to list', (val) => parseInt(val, 10))
17
+ .option('--time-min <iso-date>', 'Start date ISO-8601 string')
18
+ .option('--time-max <iso-date>', 'End date ISO-8601 string')
19
+ .requiredOption('--output <path>', 'Output JSON file path')
20
+ .action(async (options) => {
21
+ const client = new GoogleCalendarClient();
22
+ const result = await client.listEvents(
23
+ options.calendarId,
24
+ options.limit,
25
+ options.timeMin,
26
+ options.timeMax
27
+ );
28
+ await Skills.writeOutput(result, options.output);
29
+ });
30
+
31
+ // Subcommand: create-event
32
+ program
33
+ .command('create-event')
34
+ .description('Create a new calendar event')
35
+ .requiredOption('--summary <title>', 'Event title')
36
+ .requiredOption('--start <datetime>', 'Event start date/time (YYYY-MM-DD HH:MM:SS)')
37
+ .option('--duration <hours>', 'Duration of the event in hours', (val) => parseFloat(val), 1.0)
38
+ .option('--description <text>', 'Event description')
39
+ .option('--calendar-id <id>', 'Calendar ID (email or primary)', 'primary')
40
+ .requiredOption('--output <path>', 'Output JSON file path')
41
+ .action(async (options) => {
42
+ const client = new GoogleCalendarClient();
43
+ const result = await client.createEvent(
44
+ options.summary,
45
+ options.start,
46
+ options.duration,
47
+ options.description,
48
+ options.calendarId
49
+ );
50
+ await Skills.writeOutput(result, options.output);
51
+ });
52
+
53
+ // Subcommand: delete-event
54
+ program
55
+ .command('delete-event')
56
+ .description('Delete a calendar event by ID')
57
+ .requiredOption('--event-id <id>', 'Event ID')
58
+ .option('--calendar-id <id>', 'Calendar ID (email or primary)', 'primary')
59
+ .requiredOption('--output <path>', 'Output JSON file path')
60
+ .action(async (options) => {
61
+ const client = new GoogleCalendarClient();
62
+ const result = await client.deleteEvent(options.eventId, options.calendarId);
63
+ await Skills.writeOutput(result, options.output);
64
+ });
65
+
66
+ await program.parseAsync(process.argv);
67
+ }
68
+
69
+ if (import.meta.main) {
70
+ main().catch((err) => {
71
+ Skills.error(`Unexpected process error: ${err.message}`);
72
+ process.exit(1);
73
+ });
74
+ }
@@ -0,0 +1,116 @@
1
+ ---
2
+ name: odoo-integration
3
+ description: >-
4
+ Allows interacting with your Odoo ERP to create or view entities such as
5
+ companies, individual contacts, business opportunities (leads/CRM), calendar appointments, and planned activities.
6
+ Trigger when the user wants to add/update clients, record sales opportunities, schedule Odoo meetings, or search active records.
7
+ ---
8
+
9
+ # Odoo Integration Skill
10
+
11
+ ## Presentation
12
+ This Skill provides a CLI tool executed via Bun to interact with Odoo ERP via its official XML-RPC API. It enables structuring client accounts by creating companies, linking contacts to those companies, logging CRM opportunities, calendar appointments, and scheduled follow-up activities.
13
+
14
+ ## Prerequisites and Credentials
15
+ The CLI automatically loads parameters from your `.env` file at the root of the sales-skills repository:
16
+ - Expected path: `/Users/crapougnax/CODE/BRAD2026/sales-skills/.env`
17
+
18
+ Please configure the following environment variables:
19
+ - `ODOO_URL`: URL of your Odoo instance (e.g. `https://your-erp.odoo.com`)
20
+ - `ODOO_DB`: Database name
21
+ - `ODOO_USER`: Connection email or login username
22
+ - `ODOO_PASSWORD`: Generated API Key from your Odoo user profile (recommended) or connection password
23
+
24
+ ## Quick Start / Usage
25
+
26
+ All commands must be executed using `bun run`. Always redirect JSON output to `.log/` folders via the `--output` option.
27
+
28
+ ### 1. Create a Company
29
+ ```bash
30
+ bun run skills/vendor/odoo/cli.ts create-company \
31
+ --name "Acme Corporation" \
32
+ --email "billing@acme.com" \
33
+ --phone "+33102030405" \
34
+ --city "Paris" \
35
+ --country "France" \
36
+ --street "123 Main Street" \
37
+ --output ".log/company_result.json"
38
+ ```
39
+
40
+ ### 2. Create a Contact (linked to a company)
41
+ ```bash
42
+ bun run skills/vendor/odoo/cli.ts create-contact \
43
+ --name "Alice Smith" \
44
+ --company-id 1234 \
45
+ --email "alice@acme.com" \
46
+ --phone "+33600000000" \
47
+ --function "Purchasing Manager" \
48
+ --street "123 Main Street" \
49
+ --output ".log/contact_result.json"
50
+ ```
51
+
52
+ ### 3. Update a Partner (Company or Contact)
53
+ ```bash
54
+ bun run skills/vendor/odoo/cli.ts update-partner \
55
+ --id 1234 \
56
+ --phone "+33611223344" \
57
+ --company-id 5678 \
58
+ --output ".log/update_result.json"
59
+ ```
60
+
61
+ ### 4. Create a CRM Opportunity
62
+ ```bash
63
+ bun run skills/vendor/odoo/cli.ts create-opportunity \
64
+ --name "Enterprise ERP License Proposal" \
65
+ --partner-id 1234 \
66
+ --revenue 45000 \
67
+ --description "12-month software subscription" \
68
+ --output ".log/opp_result.json"
69
+ ```
70
+
71
+ ### 5. Create a Meeting (Calendar Event)
72
+ ```bash
73
+ bun run skills/vendor/odoo/cli.ts create-meeting \
74
+ --name "Project Review" \
75
+ --start "2026-06-25 10:00:00" \
76
+ --duration 1.0 \
77
+ --partner-ids "1234,5678" \
78
+ --output ".log/meeting_result.json"
79
+ ```
80
+ *Note: Start date must be provided in UTC format: `YYYY-MM-DD HH:MM:SS`.*
81
+
82
+ ### 6. Create a Planned Activity
83
+ ```bash
84
+ bun run skills/vendor/odoo/cli.ts create-activity \
85
+ --model "res.partner" \
86
+ --res-id 1234 \
87
+ --summary "Send technical quote" \
88
+ --note "Quote detailing the scope of work" \
89
+ --type "todo" \
90
+ --output ".log/activity_result.json"
91
+ ```
92
+
93
+ ### 7. Search Partners
94
+ ```bash
95
+ bun run skills/vendor/odoo/cli.ts search-partners \
96
+ --query "Acme" \
97
+ --is-company true \
98
+ --limit 10 \
99
+ --output ".log/search_partners_result.json"
100
+ ```
101
+
102
+ ### 8. Search CRM Opportunities
103
+ ```bash
104
+ bun run skills/vendor/odoo/cli.ts search-opportunities \
105
+ --query "ERP" \
106
+ --limit 10 \
107
+ --output ".log/search_opportunities_result.json"
108
+ ```
109
+
110
+ ## Common Issues & Guidelines
111
+ 1. **API Credentials**: Ensure your `.env` file variables match your Odoo user profile API credentials.
112
+ 2. **Entity Ordering**: When doing a complete ingestion:
113
+ - Step A: Create/find the Company (to get its `id`).
114
+ - Step B: Create/find the Contact, linking it to the Company ID.
115
+ - Step C: Create the CRM Opportunity, linking it to the Contact or Company.
116
+ 3. **Automatic Stop Time**: The `create-meeting` command automatically computes the `stop` datetime to prevent validation mismatch errors.