Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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
8 changes: 3 additions & 5 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ name: Publish to npm
on:
push:
tags:
- 'v*'
- "v*"

jobs:
publish:
Expand All @@ -23,8 +23,8 @@ jobs:
- name: Setup Node for npm
uses: actions/setup-node@v4
with:
node-version: '24'
registry-url: 'https://registry.npmjs.org'
node-version: "24"
registry-url: "https://registry.npmjs.org"

- name: Install dependencies
run: bun install
Expand All @@ -43,5 +43,3 @@ jobs:

- name: Publish to npm
run: npm publish --provenance --access public
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
11 changes: 9 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ intercom contact delete <id>
intercom conversation list
intercom conversation search --state open
intercom conversation get <id>
intercom conversation reply <id> --admin <admin-id> --body "Thank you!"
intercom conversation reply <id> --admin <admin-id> --body "Internal triage note" --type note
intercom conversation close <id> --admin <admin-id>

# Manage companies
Expand All @@ -86,7 +86,8 @@ intercom event track --name "purchase" --user-id "user123"
intercom ticket create --type-id 1234 --contact-id abc123 --title "Issue"
intercom ticket search --state open
intercom ticket get <id>
intercom ticket reply <id> --admin <admin-id> --body "We're on it!"
intercom ticket reply <id> --admin <admin-id> --body "We're on it!" --type comment
intercom ticket reply <id> --admin <admin-id> --body "Internal note" --json '{"message_type":"note"}'
intercom ticket close <id> --admin <admin-id>

# List ticket types
Expand Down Expand Up @@ -135,6 +136,9 @@ intercom ticket-type list
| `intercom conversation snooze <id>` | Snooze conversation |
| `intercom conversation convert <id>` | Convert conversation to ticket |

`intercom conversation reply` supports `--type <comment|note>` and `--json <json>`.
Message type precedence: `--type` > `--json.message_type` > `comment`.

### Companies

| Command | Description |
Expand Down Expand Up @@ -191,6 +195,9 @@ intercom ticket-type list
| `intercom ticket close <id>` | Close a ticket |
| `intercom ticket assign <id>` | Assign ticket to admin/team |

`intercom ticket reply` supports `--type <comment|note>` and `--json <json>`.
Message type precedence: `--type` > `--json.message_type` > `comment`.

### Ticket Types

| Command | Description |
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kyoji2/intercom-cli",
"version": "0.1.4",
"version": "0.1.5",
"description": "AI-native CLI for Intercom - manage customer conversations, contacts, messages, and support",
"author": "kyoji2",
"repository": {
Expand Down
1 change: 1 addition & 0 deletions skills/intercom/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ intercom conversation list
intercom conversation search --state open
intercom conversation get <id>
intercom conversation reply <id> --admin <admin-id> --body "Message"
intercom conversation reply <id> --admin <admin-id> --body "Internal note" --type note
intercom conversation close <id> --admin <admin-id>
intercom conversation assign <id> --admin <admin-id> --assignee <id>

Expand Down
9 changes: 8 additions & 1 deletion skills/intercom/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ compatibility: Requires Bun runtime (v1.0+) and Intercom account with API token
metadata:
author: kyoji2
homepage: https://github.com/kyoji2/intercom-cli
version: "1.1"
version: "1.2"
---

# Intercom CLI
Expand Down Expand Up @@ -113,6 +113,8 @@ intercom conversation search --assignee <admin-id>
intercom conversation search --json '{"query":{"field":"state","operator":"=","value":"open"}}'

intercom conversation reply <id> --admin <admin-id> --body "Message"
intercom conversation reply <id> --admin <admin-id> --body "Internal note" --type note
intercom conversation reply <id> --admin <admin-id> --body "Internal note" --json '{"message_type":"note"}'
intercom conversation assign <id> --admin <admin-id> --assignee <assignee-id>
intercom conversation close <id> --admin <admin-id>
intercom conversation open <id> --admin <admin-id>
Expand All @@ -123,6 +125,8 @@ intercom conversation convert <id> --type-id <ticket-type-id>
intercom conversation convert <id> --type-id <ticket-type-id> --title "Bug Report" --description "Details"
```

For `conversation reply`, message type precedence is `--type` > `--json.message_type` > `comment`.

## Tickets

```bash
Expand Down Expand Up @@ -150,6 +154,7 @@ intercom ticket update <id> --json '{"ticket_attributes":{"_default_title_":"Upd
# Reply (comment visible to customer, note internal only)
intercom ticket reply <id> --admin <admin-id> --body "We're investigating"
intercom ticket reply <id> --admin <admin-id> --body "Internal note" --type note
intercom ticket reply <id> --admin <admin-id> --body "Internal note" --json '{"message_type":"note"}'

# Workflow actions
intercom ticket close <id> --admin <admin-id>
Expand All @@ -159,6 +164,8 @@ intercom ticket assign <id> --admin <admin-id> --assignee <assignee-id>
intercom ticket delete <id>
```

For `ticket reply`, message type precedence is `--type` > `--json.message_type` > `comment`.

## Companies

```bash
Expand Down
28 changes: 28 additions & 0 deletions skills/intercom/references/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -222,12 +222,17 @@ Reply to a conversation.
|--------|-------------|----------|
| `--admin <id>` | Admin ID sending reply | Yes |
| `--body <body>` | Reply message | Yes |
| `--type <type>` | Message type (`comment`, `note`) | No |
| `--json <json>` | Additional data as JSON | No |

```bash
intercom conversation reply 12345 --admin 67890 --body "Thank you for reaching out!"
intercom conversation reply 12345 --admin 67890 --body "Internal note" --type note
intercom conversation reply 12345 --admin 67890 --body "Internal note" --json '{"message_type":"note"}'
```

Message type precedence: `--type` > `--json.message_type` > `comment`.

### `intercom conversation assign <id>`

Assign conversation to admin/team.
Expand Down Expand Up @@ -280,6 +285,29 @@ intercom conversation snooze 12345 --admin 67890 --until 1735689600

---

## Ticket Commands

### `intercom ticket reply <id>`

Reply to a ticket.

| Option | Description | Required |
|--------|-------------|----------|
| `--admin <id>` | Admin ID sending reply | Yes |
| `--body <body>` | Reply message | Yes |
| `--type <type>` | Message type (`comment`, `note`) | No |
| `--json <json>` | Additional data as JSON | No |

```bash
intercom ticket reply 12345 --admin 67890 --body "We're investigating"
intercom ticket reply 12345 --admin 67890 --body "Internal note" --type note
intercom ticket reply 12345 --admin 67890 --body "Internal note" --json '{"message_type":"note"}'
```

Message type precedence: `--type` > `--json.message_type` > `comment`.

---

## Company Commands

### `intercom company create`
Expand Down
12 changes: 10 additions & 2 deletions src/cli/registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -430,13 +430,17 @@ export function registerCommands(program: Command, ctx: RegisterContext): void {
{ flags: "--admin <id>", description: "Admin ID sending the reply" },
{ flags: "--body <body>", description: "Reply message body" },
],
options: [{ flags: "--json <json>", description: "Additional reply data as JSON" }],
options: [
{ flags: "--type <type>", description: "Message type (comment, note)" },
{ flags: "--json <json>", description: "Additional reply data as JSON" },
],
action: async ({ globals, args, options }) => {
await cmdConversationReply({
...globals,
id: String(args[0]),
adminId: options.admin as string,
body: options.body as string,
messageType: options.type as string | undefined,
json: options.json as string | undefined,
});
},
Expand Down Expand Up @@ -845,14 +849,18 @@ export function registerCommands(program: Command, ctx: RegisterContext): void {
{ flags: "--admin <id>", description: "Admin ID sending the reply" },
{ flags: "--body <body>", description: "Reply message body" },
],
options: [{ flags: "--type <type>", description: "Message type (comment, note)", defaultValue: "comment" }],
options: [
{ flags: "--type <type>", description: "Message type (comment, note)" },
{ flags: "--json <json>", description: "Additional reply data as JSON" },
],
action: async ({ globals, args, options }) => {
await cmdTicketReply({
...globals,
id: String(args[0]),
adminId: options.admin as string,
body: options.body as string,
messageType: options.type as string | undefined,
json: options.json as string | undefined,
});
},
},
Expand Down
8 changes: 8 additions & 0 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,14 @@ function getDryRunResponse(method: string): unknown {
}

export function handleIntercomError(error: unknown): never {
if (error instanceof SyntaxError) {
throw new CLIError(
"Invalid JSON input provided to command.",
400,
"Ensure your JSON data is valid and properly escaped for the shell.",
);
}

if (error instanceof IntercomError) {
let hint: string | undefined;
if (error.statusCode === 401) {
Expand Down
12 changes: 7 additions & 5 deletions src/commands/conversations.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ora from "ora";
import { createClient, handleIntercomError } from "../client.ts";
import { CLIError, type GlobalOptions, getTokenAsync, output } from "../utils/index.ts";
import { buildAdminReplyPayload } from "./replyPayload.ts";

export interface ConversationListOptions extends GlobalOptions {
limit?: string;
Expand All @@ -21,6 +22,7 @@ export interface ConversationReplyOptions extends GlobalOptions {
id: string;
adminId: string;
body: string;
messageType?: string;
json?: string;
}

Expand Down Expand Up @@ -208,12 +210,12 @@ export async function cmdConversationReply(options: ConversationReplyOptions): P

const result = await client.conversations.reply({
conversation_id: options.id,
body: {
message_type: "comment",
type: "admin",
admin_id: options.adminId,
body: buildAdminReplyPayload({
adminId: options.adminId,
body: options.body,
},
messageType: options.messageType,
json: options.json,
}),
});

spinner.succeed("Reply sent");
Expand Down
4 changes: 3 additions & 1 deletion src/commands/overview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,9 @@ export function cmdSchema(): void {
create_contact: 'intercom contact create --email "user@example.com" --name "John Doe"',
search_contacts: 'intercom contact search --email "user@example.com"',
list_conversations: "intercom conversation list --limit 10",
reply_conversation: 'intercom conversation reply <id> --admin <admin-id> --body "Thank you!"',
reply_conversation: 'intercom conversation reply <id> --admin <admin-id> --body "Internal note" --type note',
reply_ticket:
'intercom ticket reply <id> --admin <admin-id> --body "Internal note" --json \'{"message_type":"note"}\'',
create_tag: 'intercom tag create "VIP Customer"',
search_articles: 'intercom article search "getting started"',
},
Expand Down
46 changes: 46 additions & 0 deletions src/commands/replyPayload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { CLIError } from "../utils/index.ts";

type ReplyPayloadInput = {
adminId: string;
body: string;
messageType?: string;
json?: string;
};

export type ReplyBodyPayload = {
message_type: "comment" | "note";
type: "admin";
admin_id: string;
body: string;
} & Record<string, unknown>;

function validateReplyType(value: unknown, source: "--type" | "--json"): "comment" | "note" {
if (value !== "comment" && value !== "note") {
throw new CLIError(`Invalid ${source} value for message type: ${String(value)}`, 400, "Use comment or note.");
}
return value;
}

export function buildAdminReplyPayload(input: ReplyPayloadInput): ReplyBodyPayload {
const parsed = input.json ? JSON.parse(input.json) : {};

if (parsed === null || Array.isArray(parsed) || typeof parsed !== "object") {
throw new CLIError("Invalid --json value. Expected a JSON object.", 400);
}

const messageTypeFromJson =
Object.hasOwn(parsed, "message_type") && (parsed as Record<string, unknown>).message_type !== undefined
? validateReplyType((parsed as Record<string, unknown>).message_type, "--json")
: undefined;
const messageType = input.messageType
? validateReplyType(input.messageType, "--type")
: (messageTypeFromJson ?? "comment");

return {
...(parsed as Record<string, unknown>),
message_type: messageType,
type: "admin",
admin_id: input.adminId,
body: input.body,
};
}
14 changes: 7 additions & 7 deletions src/commands/tickets.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import ora from "ora";
import { createClient, handleIntercomError } from "../client.ts";
import { CLIError, type GlobalOptions, getTokenAsync, output } from "../utils/index.ts";
import { buildAdminReplyPayload } from "./replyPayload.ts";

export interface TicketGetOptions extends GlobalOptions {
id: string;
Expand Down Expand Up @@ -42,6 +43,7 @@ export interface TicketReplyOptions extends GlobalOptions {
adminId: string;
body: string;
messageType?: string;
json?: string;
}

export interface TicketCloseOptions extends GlobalOptions {
Expand Down Expand Up @@ -306,16 +308,14 @@ export async function cmdTicketReply(options: TicketReplyOptions): Promise<void>
try {
const client = createClient({ token, dryRun: options.dryRun });

const messageType = options.messageType === "note" ? "note" : "comment";

const result = await client.tickets.reply({
ticket_id: options.id,
body: {
message_type: messageType,
type: "admin",
admin_id: options.adminId,
body: buildAdminReplyPayload({
adminId: options.adminId,
body: options.body,
},
messageType: options.messageType,
json: options.json,
}),
});

spinner.succeed("Reply sent");
Expand Down
14 changes: 14 additions & 0 deletions tests/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,13 @@ describe("CLI Integration", () => {
expect(stdout).toContain("close");
});

test("conversation reply --help shows --type and --json", async () => {
const { stdout } = await cli("conversation reply --help");

expect(stdout).toContain("--type");
expect(stdout).toContain("--json");
});

test("company --help shows subcommands", async () => {
const { stdout } = await cli("company --help");

Expand Down Expand Up @@ -112,6 +119,13 @@ describe("CLI Integration", () => {
expect(stdout).toContain("list");
expect(stdout).toContain("get");
});

test("ticket reply --help shows --type and --json", async () => {
const { stdout } = await cli("ticket reply --help");

expect(stdout).toContain("--type");
expect(stdout).toContain("--json");
});
});

describe("schema command", () => {
Expand Down
Loading