@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.
- package/.env.dist +18 -0
- package/AGENT.md +58 -0
- package/HOWTO.md +72 -0
- package/LICENSE +8 -0
- package/LLM.md +37 -0
- package/README.md +100 -0
- package/bin/activate_skills.sh +54 -0
- package/bin/lm_studio_agent.ts +172 -0
- package/bin/publish-prepare.cjs +69 -0
- package/bun.lock +262 -0
- package/package.json +35 -0
- package/skills/actions/sync-meetings-to-odoo/SKILL.md +97 -0
- package/skills/actions/sync-meetings-to-odoo/scripts/sync_meetings_cli.ts +235 -0
- package/skills/vendor/brevo/SKILL.md +83 -0
- package/skills/vendor/brevo/cli.ts +167 -0
- package/skills/vendor/google/SKILL.md +66 -0
- package/skills/vendor/google/calendar.ts +150 -0
- package/skills/vendor/google/cli.ts +74 -0
- package/skills/vendor/odoo/SKILL.md +116 -0
- package/skills/vendor/odoo/cli.ts +566 -0
- package/tsconfig.json +24 -0
|
@@ -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.
|