Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
157 changes: 157 additions & 0 deletions hubspot-crm-skill/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
# HubspotCRMSkills
# ⚡ HubSpot CRM Skill — Lua AI Agent

A lightweight Lua AI Agent skill that allows your agent to **create/update contacts in HubSpot CRM** using a secure Private App token.

## 🚀 Quick Start

```bash
# Install dependencies
npm install

# Build the project
npm run build

# Start interactive sandbox chat
lua chat

# Push skill to server
lua push

# Deploy to production (only when ready)
lua push --deploy
```

---

## 📁 Project Structure

```
hubspot-skill/
├── src/
│ ├── index.ts # Agent configuration
│ ├── skills/
│ │ ├── hubspotCRM.skill.ts # Skill definition
│ │ └── createContact.ts # Tool: create HubSpot contact
├── .env.example # Environment variable template
├── lua.skill.yaml # Auto-generated agent metadata
Comment thread
Kurume98 marked this conversation as resolved.
Outdated
├── package.json # Project config
├── tsconfig.json # TypeScript settings
└── README.md # This file
```

---

## 🔧 Environment Variables

Create a `.env` file:

```
HUBSPOT_PRIVATE_APP_TOKEN=your-token-here
HUBSPOT_API_BASE_URL=https://api.hubapi.com
```

Set in production using:

```bash
lua env production
# ➕ Add new variable:
# Name: HUBSPOT_PRIVATE_APP_TOKEN
# Value: <your-token>
```

---

## 🛠️ Tool: Create Contact

This project includes **1 tool**:

### `createContact`
Creates a new contact in HubSpot CRM using your private app token.

Input fields:
- `email` *(optional but recommended)*
- `firstname`
- `lastname`
- `phone` *(optional)*
- `company` *(optional)*

Example prompt in lua chat:

```
Create a contact with email test@example.com, firstname Test, lastname User.
```

---

## 🧠 Skill: HubSpot CRM

The skill groups all HubSpot-related tools and gives your agent the ability to talk to HubSpot's CRM API.

Location:
```
src/skills/hubspotCRM.skill.ts
```

---

## 🤖 Agent Configuration

Located in:
```
src/index.ts
```

Defines:
- Agent name
- Persona
- Skills registered
- Runtime behavior

You can test locally using:

```bash
lua chat
```

---

## 🧪 Testing

### Test only the tool:
```bash
lua test
```

### Test entire agent (sandbox):
```bash
lua chat
```

### Test production after deployment:
```bash
lua chat
# Select 🚀 Production
```

---

## 🛡️ Best Practices

1. **Never commit your `.env` file to GitHub**
2. Keep each tool focused on one task
3. Test in sandbox before pushing to production
4. Ensure your HubSpot private app has the following scopes:
- `crm.objects.contacts.write`
- `crm.objects.contacts.read`

---

## 💬 Support

- Lua Docs: https://docs.heylua.ai
- HubSpot API Docs: https://developers.hubspot.com/docs/api/overview

---

*Skill created using **Lua CLI** and the HubSpot CRM API.*
20 changes: 20 additions & 0 deletions hubspot-crm-skill/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{
Comment thread
Kurume98 marked this conversation as resolved.
"$schema": "https://json.schemastore.org/package.json",
"name": "hubspot-skill",
"version": "1.0.0",
"scripts": {
"dev": "lua dev",
"build": "tsc",
"compile": "lua compile"
},
"dependencies": {
"lua-cli": "^1.0.0",
Comment thread
Kurume98 marked this conversation as resolved.
Outdated
"node-fetch": "^2.6.7",
Comment thread
Kurume98 marked this conversation as resolved.
Outdated
"zod": "^3.23.2"
},
"devDependencies": {
"@types/node": "^25.0.0",
"tsx": "^3.12.7",
"typescript": "^4.9.5"
}
}
22 changes: 22 additions & 0 deletions hubspot-crm-skill/src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// src/index.ts
// DO NOT import runtime classes from "lua-cli".
// Lua CLI injects real runtime versions at bundle/test/run time.

declare const LuaAgent: any;
Comment thread
Kurume98 marked this conversation as resolved.
Outdated

import hubspotCRM from "./skills/hubspotCRM.skill";

const agent = new LuaAgent({
name: "Hubspot-crm-skill-agent",
persona:
"Sophie Connect - Lead CRM Integration Specialist. Builds and operates CRM integrations and semantic middleware for context-aware routing.",
skills: [hubspotCRM], // <-- this is correct; makes the skill visible
});

async function main() {
console.log("Agent configured");
}

main().catch(console.error);

export default agent;
69 changes: 69 additions & 0 deletions hubspot-crm-skill/src/skills/createContact.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
// src/skills/createContact.ts
import fetch from "node-fetch";
Comment thread
Kurume98 marked this conversation as resolved.
Outdated

export type CreateInput = {
email?: string;
firstname?: string;
lastname?: string;
phone?: string;
company?: string;
};

const HUBSPOT_BASE =
(process.env.HUBSPOT_API_BASE_URL || "https://api.hubapi.com").replace(/\/+$/, "");

const TOKEN = process.env.HUBSPOT_PRIVATE_APP_TOKEN;
Comment thread
Kurume98 marked this conversation as resolved.
Outdated

export default async function createContact(input: CreateInput) {
if (!TOKEN) {
Comment thread
Kurume98 marked this conversation as resolved.
Outdated
return {
ok: false,
error: "Missing HUBSPOT_PRIVATE_APP_TOKEN in environment.",
};
}

const properties: Record<string, any> = {};

if (input.email) properties.email = input.email;
if (input.firstname) properties.firstname = input.firstname;
if (input.lastname) properties.lastname = input.lastname;
if (input.phone) properties.phone = input.phone;
if (input.company) properties.company = input.company;

if (Object.keys(properties).length === 0) {
return { ok: false, error: "No properties provided." };
}

const url = `${HUBSPOT_BASE}/crm/v3/objects/contacts`;

try {
const res = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${TOKEN}`,
"Content-Type": "application/json",
Accept: "application/json",
},
body: JSON.stringify({ properties }),
});

const body: any = await res.json();

if (!res.ok) {
return {
ok: false,
status: res.status,
error: body?.message ?? JSON.stringify(body),
};
}

return {
ok: true,
id: body?.id,
properties: body?.properties ?? {},
raw: body,
};
} catch (err: any) {
return { ok: false, error: err.message ?? String(err) };
}
}
61 changes: 61 additions & 0 deletions hubspot-crm-skill/src/skills/hubspotCRM.skill.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// src/skills/hubspotCRM.skill.ts
import { z } from "zod";
import createContact from "./createContact";

/**
* Do NOT import runtime classes from "lua-cli" at compile-time.
* The CLI injects real runtime objects at bundling/execution time.
*
* Provide *type* aliases for compile-time only so `implements` works
* without TS confusing a runtime value with a type.
*/
export type LuaToolType = any;
Comment thread
Kurume98 marked this conversation as resolved.
Outdated
export type LuaSkillType = any;

/**
* Top-level exported zod schema (stable name) so the bundler can extract it.
* Must be an exported const with the exact name referenced below.
*/
export const createContactInputSchema = z.object({
email: z.string().email().optional(),
firstname: z.string().optional(),
lastname: z.string().optional(),
phone: z.string().optional(),
company: z.string().optional(),
});

class CreateContactTool implements LuaToolType {
Comment thread
Kurume98 marked this conversation as resolved.
Outdated
name = "createContact";
description = "Creates a new contact in HubSpot CRM";

// Must reference the SAME top-level exported zod const name above.
inputSchema = createContactInputSchema;

async execute(input: any) {
// zod parsing keeps runtime safe
const parsed = input ? this.inputSchema.parse(input) : {};
return await createContact(parsed);
}
}

/**
* NOTE: do NOT import `LuaSkill` from lua-cli at compile time.
* Use the `LuaSkill` type alias above only for typing.
*
* The CLI will inject the real `LuaSkill` value at runtime.
*/
const hubspotCRM = new (((globalThis as any).LuaSkill) || (class {}))({
Comment thread
Kurume98 marked this conversation as resolved.
Outdated
// the CLI replaces this at runtime — but this way TypeScript compiles
// If the runtime doesn't provide LuaSkill here, the CLI's bundling will
// instantiate a real LuaSkill when running inside the Lua runtime.
name: "hubspotCRM",
description: "Skill for creating contacts in HubSpot CRM",
context: "Use createContact to create a new contact in HubSpot. Provide email or firstname+lastname.",
Comment thread
Kurume98 marked this conversation as resolved.
Outdated
tools: [new CreateContactTool()],
}) as any;

/**
* Export default must be a valid value the bundler can find.
* If the runtime/cli will inject LuaSkill, the CLI will handle wiring.
*/
export default hubspotCRM;
11 changes: 11 additions & 0 deletions hubspot-crm-skill/src/types/lua-cli.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// src/types/lua-cli.d.ts
Comment thread
Kurume98 marked this conversation as resolved.
Outdated
declare module "lua-cli" {
// runtime objects provided by the lua CLI - typed as `any` for compile-time safety
export const LuaAgent: any;
export const LuaSkill: any;
export const LuaTool: any;

// if you import a default or named, add them too:
const _default: any;
export default _default;
}
18 changes: 18 additions & 0 deletions hubspot-crm-skill/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2020", "DOM"],
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"forceConsistentCasingInFileNames": true,
"strict": true,
"skipLibCheck": true,
"baseUrl": ".",
"outDir": "./dist",
"types": ["node"]
},
"exclude": ["dist", "node_modules"]
}