mirror of
https://github.com/cline/cline.git
synced 2026-09-15 21:04:27 +08:00
Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62165298aa | ||
|
|
722f3f2d38 |
@@ -441,12 +441,13 @@ See [DISTRIBUTION.md](./DISTRIBUTION.md) for details on how the CLI is packaged.
|
||||
|
||||
### Connector runtime behavior
|
||||
|
||||
- Telegram final assistant replies are sent through Telegram entity payloads with raw-text fallback; Google Chat and WhatsApp use the shared connector runtime formatting path.
|
||||
- Telegram final assistant replies are sent through Telegram entity payloads with raw-text fallback; AgentPhone, Google Chat, and WhatsApp use the shared connector runtime formatting path.
|
||||
- Assistant text streams incrementally into chat surfaces that use the shared runtime streaming path; Telegram sends final assistant replies after the turn completes.
|
||||
- Tool activity is summarized as compact start/error messages with short argument previews.
|
||||
- Required tool approvals are posted back into the chat thread and accept `Y` / `N` replies.
|
||||
- Google Chat serves its webhook at `/api/webhooks/gchat`; configure the Google Chat App URL as `<base-url>/api/webhooks/gchat`.
|
||||
- Webhook-based connectors are hosted through a shared CLI `node:http` server helper rather than `Bun.serve`.
|
||||
- AgentPhone verifies startup with `/v1/numbers`, stores the active number assigned to the agent, and serves its webhook at `/api/webhooks/agentphone`; configure the AgentPhone webhook URL as `<base-url>/api/webhooks/agentphone`.
|
||||
- WhatsApp serves its webhook at `/api/webhooks/whatsapp`; configure the Meta callback URL as `<base-url>/api/webhooks/whatsapp`.
|
||||
|
||||
## Logging adapter
|
||||
|
||||
+4
-1
@@ -165,9 +165,12 @@ cline auth --provider openai-native --apikey sk-... --modelid gpt-5 --baseurl ht
|
||||
|
||||
### Connectors
|
||||
|
||||
Bridge a chat surface into RPC-backed Cline sessions. Each conversation thread maps to a session with full context. Supported platforms: Telegram, Slack, Google Chat, WhatsApp, and Linear.
|
||||
Bridge a chat surface into RPC-backed Cline sessions. Each conversation thread maps to a session with full context. Supported platforms: AgentPhone, Telegram, Slack, Google Chat, WhatsApp, and Linear. AgentPhone startup verifies the API key with `/v1/numbers` and stores the active phone number assigned to the agent.
|
||||
|
||||
```sh
|
||||
# AgentPhone (webhook mode)
|
||||
cline connect agentphone --api-key $AGENTPHONE_API_KEY --agent-id $AGENTPHONE_AGENT_ID --webhook-secret $AGENTPHONE_WEBHOOK_SECRET --base-url https://your-domain.com
|
||||
|
||||
# Telegram (polling mode)
|
||||
cline connect telegram -k 123456:ABCDEF...
|
||||
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
"@agentphone/chat-sdk-adapter": "^0.1.0",
|
||||
"@chat-adapter/discord": "^4.23.0",
|
||||
"@chat-adapter/gchat": "^4.23.0",
|
||||
"@chat-adapter/linear": "^4.23.0",
|
||||
@@ -80,10 +81,11 @@
|
||||
"@opentui-ui/dialog": "^0.1.2",
|
||||
"@opentui/core": "0.1.102",
|
||||
"@opentui/react": "0.1.102",
|
||||
"chat": "^4.23.0",
|
||||
"chat": "^4.30.0",
|
||||
"commander": "^14.0.3",
|
||||
"fzf": "^0.5.2",
|
||||
"marked": "^15.0.12",
|
||||
"nanoid": "^5.1.7",
|
||||
"open": "^10.2.0",
|
||||
"opentui-spinner": "^0.0.6",
|
||||
"pino": "^10.3.1",
|
||||
@@ -91,7 +93,6 @@
|
||||
"react-devtools-core": "^7.0.1",
|
||||
"react-reconciler": "0.32.0",
|
||||
"yaml": "^2.8.2",
|
||||
"nanoid": "^5.1.7",
|
||||
"zod": "^4.1.11"
|
||||
},
|
||||
"devDependencies": {
|
||||
|
||||
@@ -0,0 +1,469 @@
|
||||
import { createHmac } from "node:crypto";
|
||||
import type { ConnectAgentPhoneOptions } from "@cline/shared";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import { __test__, agentPhoneConnector } from "./agentphone";
|
||||
|
||||
const parseAgentPhoneArgs = (rawArgs: string[]): ConnectAgentPhoneOptions =>
|
||||
(
|
||||
agentPhoneConnector as unknown as {
|
||||
parseArgs(rawArgs: string[]): ConnectAgentPhoneOptions;
|
||||
}
|
||||
).parseArgs(rawArgs);
|
||||
|
||||
describe("agentPhoneConnector", () => {
|
||||
it("parses AgentPhone credentials separately from provider credentials", () => {
|
||||
const options = parseAgentPhoneArgs([
|
||||
"--api-key",
|
||||
"agentphone-key",
|
||||
"--agent-id",
|
||||
"agent_123",
|
||||
"--webhook-secret",
|
||||
"whsec_123",
|
||||
"--provider-api-key",
|
||||
"provider-key",
|
||||
"--base-url",
|
||||
"https://example.test",
|
||||
]);
|
||||
|
||||
expect(options.apiKey).toBe("agentphone-key");
|
||||
expect(options.agentId).toBe("agent_123");
|
||||
expect(options.webhookSecret).toBe("whsec_123");
|
||||
expect(options.apiProviderKey).toBe("provider-key");
|
||||
expect(options.baseUrl).toBe("https://example.test");
|
||||
expect(options.userName).toBeUndefined();
|
||||
});
|
||||
|
||||
it("selects the active AgentPhone number for the configured agent", () => {
|
||||
const selected = __test__.selectAgentPhoneNumber({
|
||||
agentId: "agent_123",
|
||||
numbers: [
|
||||
{
|
||||
id: "num_1",
|
||||
phoneNumber: "+15550000001",
|
||||
status: "inactive",
|
||||
agentId: "agent_123",
|
||||
},
|
||||
{
|
||||
id: "num_2",
|
||||
phoneNumber: "+15550000002",
|
||||
status: "active",
|
||||
type: "sms",
|
||||
agentId: "agent_123",
|
||||
},
|
||||
{
|
||||
id: "num_3",
|
||||
phoneNumber: "+15550000003",
|
||||
status: "active",
|
||||
agentId: "other_agent",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expect(selected).toMatchObject({
|
||||
id: "num_2",
|
||||
phoneNumber: "+15550000002",
|
||||
});
|
||||
});
|
||||
|
||||
it("verifies the AgentPhone API key by fetching the agent number", async () => {
|
||||
const fetchImpl = vi.fn(async () =>
|
||||
Response.json({
|
||||
data: [
|
||||
{
|
||||
id: "cmpyckhh106qzf4t0ct7xxgye",
|
||||
phoneNumber: "+18166776225",
|
||||
country: "US",
|
||||
status: "active",
|
||||
type: "sms",
|
||||
agentId: "cmpyckevf06qxf4t0y3bl2c69",
|
||||
createdAt: "2026-06-03T17:35:29.654000Z",
|
||||
},
|
||||
],
|
||||
hasMore: false,
|
||||
total: 1,
|
||||
}),
|
||||
);
|
||||
|
||||
const number = await __test__.fetchAgentPhoneNumber({
|
||||
apiKey: "agentphone-key",
|
||||
agentId: "cmpyckevf06qxf4t0y3bl2c69",
|
||||
apiUrl: "https://api.agentphone.ai/",
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
});
|
||||
|
||||
expect(fetchImpl).toHaveBeenCalledWith(
|
||||
"https://api.agentphone.ai/v1/numbers",
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({
|
||||
Authorization: "Bearer agentphone-key",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
expect(number).toMatchObject({
|
||||
id: "cmpyckhh106qzf4t0ct7xxgye",
|
||||
phoneNumber: "+18166776225",
|
||||
type: "sms",
|
||||
});
|
||||
});
|
||||
|
||||
it("fails AgentPhone verification when no active number is assigned", async () => {
|
||||
const fetchImpl = vi.fn(async () =>
|
||||
Response.json({
|
||||
data: [
|
||||
{
|
||||
id: "num_1",
|
||||
phoneNumber: "+15550000001",
|
||||
status: "active",
|
||||
agentId: "other_agent",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
__test__.fetchAgentPhoneNumber({
|
||||
apiKey: "agentphone-key",
|
||||
agentId: "agent_123",
|
||||
fetchImpl: fetchImpl as unknown as typeof fetch,
|
||||
}),
|
||||
).rejects.toThrow(
|
||||
"AgentPhone API verification failed: no phone number is assigned to agent agent_123.",
|
||||
);
|
||||
});
|
||||
|
||||
it("resolves inbound SMS participants from contact data", () => {
|
||||
const participant = __test__.resolveAgentPhoneParticipant({
|
||||
messageId: "msg_123",
|
||||
conversationId: "conv_123",
|
||||
numberId: "num_123",
|
||||
from: "+15551234567",
|
||||
to: "+15557654321",
|
||||
contact: {
|
||||
id: "contact_123",
|
||||
name: "Alice",
|
||||
email: null,
|
||||
phoneNumber: "+15551234567",
|
||||
},
|
||||
message: "hello",
|
||||
mediaUrl: null,
|
||||
mediaUrls: [],
|
||||
direction: "inbound",
|
||||
receivedAt: "2026-03-17T00:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(participant).toEqual({
|
||||
key: "agentphone:user:+15551234567",
|
||||
label: "Alice",
|
||||
});
|
||||
});
|
||||
|
||||
it("normalizes iMessage email participants", () => {
|
||||
const participant = __test__.resolveAgentPhoneParticipant({
|
||||
messageId: "msg_123",
|
||||
conversationId: "conv_123",
|
||||
numberId: "num_123",
|
||||
from: "Alice@Example.COM",
|
||||
to: "bot@example.com",
|
||||
contact: null,
|
||||
message: "hello",
|
||||
mediaUrl: null,
|
||||
mediaUrls: [],
|
||||
direction: "inbound",
|
||||
receivedAt: "2026-03-17T00:00:00.000Z",
|
||||
});
|
||||
|
||||
expect(participant?.key).toBe("agentphone:user:alice@example.com");
|
||||
});
|
||||
|
||||
it("resolves inbound SMS message payloads into synchronous webhook turns", () => {
|
||||
const turn = __test__.resolveAgentPhoneMessageTurnPayload({
|
||||
event: "agent.message",
|
||||
channel: "sms",
|
||||
timestamp: "2026-06-03T19:19:06.000Z",
|
||||
agentId: "agent_123",
|
||||
data: {
|
||||
messageId: "msg_123",
|
||||
conversationId: "conv_123",
|
||||
numberId: "num_123",
|
||||
from: "+15551234567",
|
||||
to: "+15557654321",
|
||||
contact: null,
|
||||
message: "Can Cline help?",
|
||||
mediaUrl: null,
|
||||
mediaUrls: [],
|
||||
direction: "inbound",
|
||||
receivedAt: "2026-06-03T19:19:06.000Z",
|
||||
},
|
||||
});
|
||||
|
||||
expect(turn).toMatchObject({
|
||||
threadId: "agentphone:+15557654321:+15551234567",
|
||||
text: "Can Cline help?",
|
||||
rawMessage: {
|
||||
messageId: "msg_123",
|
||||
conversationId: "conv_123",
|
||||
from: "+15551234567",
|
||||
to: "+15557654321",
|
||||
message: "Can Cline help?",
|
||||
direction: "inbound",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("formats AgentPhone request errors for webhook responses", () => {
|
||||
expect(
|
||||
__test__.formatAgentPhoneRequestError(
|
||||
new Error(
|
||||
"Auth failed: Outbound SMS is not enabled for this account. Complete 10DLC registration first.",
|
||||
),
|
||||
),
|
||||
).toBe(
|
||||
"Request failed. Outbound SMS is not enabled for this account. Complete 10DLC registration first.",
|
||||
);
|
||||
});
|
||||
|
||||
it("returns JSON acknowledgements for non-JSON AgentPhone adapter responses", async () => {
|
||||
const response = await __test__.normalizeAgentPhoneWebhookResponse({
|
||||
response: new Response("OK", { status: 200 }),
|
||||
payload: {
|
||||
event: "agent.message",
|
||||
channel: "sms",
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.headers.get("content-type")).toContain("application/json");
|
||||
await expect(response.json()).resolves.toEqual({ ok: true });
|
||||
});
|
||||
|
||||
it("posts non-voice AgentPhone replies through the thread", async () => {
|
||||
const post = vi.fn(async () => undefined);
|
||||
const logger = {
|
||||
core: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
};
|
||||
const io = { writeln: vi.fn() };
|
||||
|
||||
await __test__.postAgentPhoneReply({
|
||||
thread: {
|
||||
id: "agentphone:+15557654321:+15551234567",
|
||||
channelId: "agentphone:+15557654321",
|
||||
post,
|
||||
} as unknown as Parameters<
|
||||
typeof __test__.postAgentPhoneReply
|
||||
>[0]["thread"],
|
||||
text: " hello from cline ",
|
||||
logger: logger as unknown as Parameters<
|
||||
typeof __test__.postAgentPhoneReply
|
||||
>[0]["logger"],
|
||||
io: io as unknown as Parameters<
|
||||
typeof __test__.postAgentPhoneReply
|
||||
>[0]["io"],
|
||||
});
|
||||
|
||||
expect(post).toHaveBeenCalledWith("hello from cline");
|
||||
expect(logger.core.log).toHaveBeenCalledWith(
|
||||
"AgentPhone outbound reply sent",
|
||||
expect.objectContaining({
|
||||
outputLength: "hello from cline".length,
|
||||
}),
|
||||
);
|
||||
expect(logger.core.error).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("surfaces outbound AgentPhone reply send failures", async () => {
|
||||
const error = new Error("AgentPhone API rejected the message");
|
||||
const post = vi.fn(async () => {
|
||||
throw error;
|
||||
});
|
||||
const logger = {
|
||||
core: {
|
||||
log: vi.fn(),
|
||||
error: vi.fn(),
|
||||
},
|
||||
};
|
||||
const io = { writeln: vi.fn() };
|
||||
|
||||
await expect(
|
||||
__test__.postAgentPhoneReply({
|
||||
thread: {
|
||||
id: "agentphone:+15557654321:+15551234567",
|
||||
channelId: "agentphone:+15557654321",
|
||||
post,
|
||||
} as unknown as Parameters<
|
||||
typeof __test__.postAgentPhoneReply
|
||||
>[0]["thread"],
|
||||
text: "hello from cline",
|
||||
logger: logger as unknown as Parameters<
|
||||
typeof __test__.postAgentPhoneReply
|
||||
>[0]["logger"],
|
||||
io: io as unknown as Parameters<
|
||||
typeof __test__.postAgentPhoneReply
|
||||
>[0]["io"],
|
||||
}),
|
||||
).rejects.toThrow("AgentPhone API rejected the message");
|
||||
|
||||
expect(logger.core.error).toHaveBeenCalledWith(
|
||||
"AgentPhone outbound reply failed",
|
||||
expect.objectContaining({
|
||||
error,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("returns voice text for non-JSON voice webhook responses", async () => {
|
||||
const response = await __test__.normalizeAgentPhoneWebhookResponse({
|
||||
response: new Response("OK", { status: 200 }),
|
||||
payload: {
|
||||
event: "agent.message",
|
||||
channel: "voice",
|
||||
},
|
||||
});
|
||||
|
||||
await expect(response.json()).resolves.toEqual({
|
||||
text: "Cline is connected. I will process the call transcript when this call ends.",
|
||||
});
|
||||
});
|
||||
|
||||
it("preserves non-OK webhook response status as JSON", async () => {
|
||||
const response = await __test__.normalizeAgentPhoneWebhookResponse({
|
||||
response: new Response("Invalid signature", { status: 401 }),
|
||||
payload: undefined,
|
||||
});
|
||||
|
||||
expect(response.status).toBe(401);
|
||||
await expect(response.json()).resolves.toEqual({
|
||||
error: "Invalid signature",
|
||||
});
|
||||
});
|
||||
|
||||
it("streams interim and final voice responses as NDJSON", async () => {
|
||||
const response = __test__.agentPhoneVoiceResponse(
|
||||
async () => "The answer is 42.",
|
||||
);
|
||||
|
||||
expect(response.headers.get("content-type")).toContain(
|
||||
"application/x-ndjson",
|
||||
);
|
||||
const lines = (await response.text())
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => JSON.parse(line) as unknown);
|
||||
expect(lines).toEqual([
|
||||
{ text: "One moment, let me check.", interim: true },
|
||||
{ text: "The answer is 42." },
|
||||
]);
|
||||
});
|
||||
|
||||
it("resolves AgentPhone live voice message payloads into turn input", () => {
|
||||
const turn = __test__.resolveAgentPhoneVoiceTurnPayload({
|
||||
event: "agent.message",
|
||||
channel: "voice",
|
||||
timestamp: "2026-06-03T19:19:06.000Z",
|
||||
agentId: "agent_123",
|
||||
data: {
|
||||
conversationId: "conv_123",
|
||||
from: "+15551234567",
|
||||
to: "+15557654321",
|
||||
message: "What is my order status?",
|
||||
direction: "inbound",
|
||||
receivedAt: "2026-06-03T19:19:06.000Z",
|
||||
},
|
||||
});
|
||||
|
||||
expect(turn).toMatchObject({
|
||||
threadId: "agentphone:+15557654321:+15551234567",
|
||||
text: "What is my order status?",
|
||||
rawMessage: {
|
||||
conversationId: "conv_123",
|
||||
from: "+15551234567",
|
||||
to: "+15557654321",
|
||||
message: "What is my order status?",
|
||||
direction: "inbound",
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it("classifies voice lifecycle webhooks separately from voice message turns", () => {
|
||||
expect(
|
||||
__test__.isAgentPhoneVoiceMessagePayload({
|
||||
event: "agent.call.started",
|
||||
channel: "voice",
|
||||
data: {},
|
||||
}),
|
||||
).toBe(false);
|
||||
expect(
|
||||
__test__.resolveAgentPhoneVoiceTurnPayload({
|
||||
event: "agent.call.started",
|
||||
channel: "voice",
|
||||
data: {},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("verifies AgentPhone webhook signatures against the raw body", () => {
|
||||
const rawBody = JSON.stringify({ event: "agent.message" });
|
||||
const secret = "whsec_test";
|
||||
const signature = `sha256=${createHmac("sha256", secret)
|
||||
.update(rawBody)
|
||||
.digest("hex")}`;
|
||||
|
||||
expect(
|
||||
__test__.verifyAgentPhoneWebhookSignature({
|
||||
rawBody,
|
||||
secret,
|
||||
signature,
|
||||
}),
|
||||
).toBe(true);
|
||||
expect(
|
||||
__test__.verifyAgentPhoneWebhookSignature({
|
||||
rawBody,
|
||||
secret,
|
||||
signature: "sha256=bad",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("rejects AgentPhone webhook signatures when no secret is configured", () => {
|
||||
expect(
|
||||
__test__.verifyAgentPhoneWebhookSignature({
|
||||
rawBody: JSON.stringify({ event: "agent.message" }),
|
||||
secret: undefined,
|
||||
signature: "sha256=abcd",
|
||||
}),
|
||||
).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("agentphone binding lookup", () => {
|
||||
it("reuses a binding by participant key across different threads", () => {
|
||||
const result = __test__.findBindingForThread(
|
||||
{
|
||||
"agentphone:user:+15551234567": {
|
||||
channelId: "agentphone:+15557654321",
|
||||
isDM: true,
|
||||
participantKey: "agentphone:user:+15551234567",
|
||||
participantLabel: "Alice",
|
||||
serializedThread: "{}",
|
||||
sessionId: "sess-1",
|
||||
state: {
|
||||
sessionId: "sess-1",
|
||||
participantKey: "agentphone:user:+15551234567",
|
||||
participantLabel: "Alice",
|
||||
},
|
||||
updatedAt: "2026-03-17T00:00:00.000Z",
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "agentphone:+15557654321:+15551234567",
|
||||
channelId: "agentphone:+15557654321",
|
||||
isDM: true,
|
||||
participantKey: "agentphone:user:+15551234567",
|
||||
},
|
||||
);
|
||||
|
||||
expect(result?.key).toBe("agentphone:user:+15551234567");
|
||||
expect(result?.binding.sessionId).toBe("sess-1");
|
||||
});
|
||||
});
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,7 +11,13 @@ import type {
|
||||
ConnectDiscordOptions,
|
||||
DiscordConnectorState,
|
||||
} from "@cline/shared";
|
||||
import { Chat, ConsoleLogger, type Thread, ThreadImpl } from "chat";
|
||||
import {
|
||||
type Adapter,
|
||||
Chat,
|
||||
ConsoleLogger,
|
||||
type Thread,
|
||||
ThreadImpl,
|
||||
} from "chat";
|
||||
import type { Command } from "commander";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
import {
|
||||
@@ -1040,7 +1046,7 @@ class DiscordConnector extends ConnectorBase<
|
||||
}) as DiscordAdapter;
|
||||
const bot = new Chat({
|
||||
userName: options.userName,
|
||||
adapters: { discord },
|
||||
adapters: { discord: discord as unknown as Adapter },
|
||||
state: new InMemoryStateAdapter(),
|
||||
logger,
|
||||
fallbackStreamingPlaceholderText: null,
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ConnectGoogleChatOptions,
|
||||
GoogleChatConnectorState,
|
||||
} from "@cline/shared";
|
||||
import { Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import { type Adapter, Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import type { Command } from "commander";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
@@ -514,7 +514,7 @@ class GoogleChatConnector extends ConnectorBase<
|
||||
);
|
||||
const bot = new Chat({
|
||||
userName: options.userName,
|
||||
adapters: { gchat },
|
||||
adapters: { gchat: gchat as unknown as Adapter },
|
||||
state: new InMemoryStateAdapter(),
|
||||
logger,
|
||||
fallbackStreamingPlaceholderText: null,
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ConnectTelegramOptions,
|
||||
TelegramConnectorState,
|
||||
} from "@cline/shared";
|
||||
import { Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import { type Adapter, Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import type { Command } from "commander";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
@@ -653,7 +653,7 @@ class TelegramConnector extends ConnectorBase<
|
||||
});
|
||||
const bot = new Chat({
|
||||
userName: options.botUsername,
|
||||
adapters: { telegram },
|
||||
adapters: { telegram: telegram as unknown as Adapter },
|
||||
state: new InMemoryStateAdapter(),
|
||||
logger,
|
||||
fallbackStreamingPlaceholderText: null,
|
||||
|
||||
@@ -8,7 +8,7 @@ import type {
|
||||
ConnectWhatsAppOptions,
|
||||
WhatsAppConnectorState,
|
||||
} from "@cline/shared";
|
||||
import { Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import { type Adapter, Chat, ConsoleLogger, type Thread } from "chat";
|
||||
import type { Command } from "commander";
|
||||
import type { CliLoggerAdapter } from "../../logging/adapter";
|
||||
import { createCliLoggerAdapter } from "../../logging/adapter";
|
||||
@@ -513,7 +513,7 @@ class WhatsAppConnector extends ConnectorBase<
|
||||
const whatsapp = createWhatsAppAdapter(whatsappConfig);
|
||||
const bot = new Chat({
|
||||
userName: options.userName,
|
||||
adapters: { whatsapp },
|
||||
adapters: { whatsapp: whatsapp as unknown as Adapter },
|
||||
state: new InMemoryStateAdapter(),
|
||||
logger,
|
||||
fallbackStreamingPlaceholderText: null,
|
||||
|
||||
@@ -4,6 +4,11 @@ export type ConnectorCatalogEntry = {
|
||||
};
|
||||
|
||||
export const CONNECTOR_CATALOG: ConnectorCatalogEntry[] = [
|
||||
{
|
||||
name: "agentphone",
|
||||
description:
|
||||
"AgentPhone SMS, MMS, iMessage, and voice webhook bridge backed by RPC runtime sessions",
|
||||
},
|
||||
{
|
||||
name: "discord",
|
||||
description:
|
||||
|
||||
@@ -85,6 +85,28 @@ async function editConnectorText(
|
||||
return await message.edit(connectorTextPayload(transport, text));
|
||||
}
|
||||
|
||||
function createCapturedConnectorMessage(
|
||||
onEdit: (text: string) => Promise<SentMessage>,
|
||||
): SentMessage {
|
||||
const message = {
|
||||
edit: async (text: string) => onEdit(text),
|
||||
delete: async () => undefined,
|
||||
addReaction: async () => undefined,
|
||||
removeReaction: async () => undefined,
|
||||
} as unknown as SentMessage;
|
||||
return message;
|
||||
}
|
||||
|
||||
async function captureConnectorText(
|
||||
sink: (text: string) => Promise<void>,
|
||||
text: string,
|
||||
): Promise<SentMessage> {
|
||||
await sink(text);
|
||||
return createCapturedConnectorMessage((nextText) =>
|
||||
captureConnectorText(sink, nextText),
|
||||
);
|
||||
}
|
||||
|
||||
function buildAttachments(input: {
|
||||
userImages: string[];
|
||||
userFiles: string[];
|
||||
@@ -259,6 +281,7 @@ export async function handleConnectorUserTurn<
|
||||
baseStartRequest: ChatStartSessionRequest,
|
||||
thread: Thread<TState>,
|
||||
) => Promise<string> | string;
|
||||
replyTextSink?: (text: string) => Promise<void>;
|
||||
postFinalReply?: (input: {
|
||||
thread: Thread<TState>;
|
||||
text: string;
|
||||
@@ -282,6 +305,17 @@ export async function handleConnectorUserTurn<
|
||||
return;
|
||||
}
|
||||
const runtimeInput = input.runtimeText?.trim() || resolvedInput;
|
||||
const postText = async (text: string): Promise<SentMessage> =>
|
||||
input.replyTextSink
|
||||
? captureConnectorText(input.replyTextSink, text)
|
||||
: postConnectorText(input.thread, input.transport, text);
|
||||
const editText = async (
|
||||
message: SentMessage,
|
||||
text: string,
|
||||
): Promise<SentMessage> =>
|
||||
input.replyTextSink
|
||||
? captureConnectorText(input.replyTextSink, text)
|
||||
: editConnectorText(message, input.transport, text);
|
||||
|
||||
const initialState = await loadThreadState(
|
||||
input.thread,
|
||||
@@ -326,7 +360,7 @@ export async function handleConnectorUserTurn<
|
||||
const denialMessage =
|
||||
authorization.message?.trim() ||
|
||||
"You are not authorized to use this bot.";
|
||||
await postConnectorText(input.thread, input.transport, denialMessage);
|
||||
await postText(denialMessage);
|
||||
await dispatchConnectorHook(
|
||||
input.hookCommand,
|
||||
{
|
||||
@@ -385,11 +419,7 @@ export async function handleConnectorUserTurn<
|
||||
input.ownerParticipantKeys,
|
||||
)
|
||||
) {
|
||||
await postConnectorText(
|
||||
input.thread,
|
||||
input.transport,
|
||||
"Only the connector owner can use slash commands.",
|
||||
);
|
||||
await postText("Only the connector owner can use slash commands.");
|
||||
input.logger.core.log("Non-owner connector chat command denied", {
|
||||
transport: input.transport,
|
||||
threadId: input.thread.id,
|
||||
@@ -439,11 +469,7 @@ export async function handleConnectorUserTurn<
|
||||
: input.firstContactMessage;
|
||||
let initialStateChanged = false;
|
||||
if (!initialState.welcomeSentAt && firstContactMessage?.trim()) {
|
||||
await postConnectorText(
|
||||
input.thread,
|
||||
input.transport,
|
||||
firstContactMessage.trim(),
|
||||
);
|
||||
await postText(firstContactMessage.trim());
|
||||
initialState.welcomeSentAt = new Date().toISOString();
|
||||
initialStateChanged = true;
|
||||
}
|
||||
@@ -482,11 +508,7 @@ export async function handleConnectorUserTurn<
|
||||
const toolLockCommand = commandName?.match(/^\/(tools|yolo)$/i);
|
||||
if (input.forceDisableTools && toolLockCommand) {
|
||||
const settingName = toolLockCommand[1]?.toLowerCase();
|
||||
await postConnectorText(
|
||||
input.thread,
|
||||
input.transport,
|
||||
`${settingName}=off (disabled by connector startup)`,
|
||||
);
|
||||
await postText(`${settingName}=off (disabled by connector startup)`);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -578,7 +600,7 @@ export async function handleConnectorUserTurn<
|
||||
);
|
||||
},
|
||||
reply: async (message) => {
|
||||
await postConnectorText(input.thread, input.transport, message);
|
||||
await postText(message);
|
||||
},
|
||||
reset: async () => {
|
||||
await clearSession({
|
||||
@@ -612,19 +634,11 @@ export async function handleConnectorUserTurn<
|
||||
abort: async () => {
|
||||
const activeTurn = input.activeTurns?.get(turnKey);
|
||||
if (!activeTurn?.sessionId?.trim()) {
|
||||
await postConnectorText(
|
||||
input.thread,
|
||||
input.transport,
|
||||
"No active task to abort.",
|
||||
);
|
||||
await postText("No active task to abort.");
|
||||
return;
|
||||
}
|
||||
await input.client.abortRuntimeSession(activeTurn.sessionId);
|
||||
await postConnectorText(
|
||||
input.thread,
|
||||
input.transport,
|
||||
"Aborting current task.",
|
||||
);
|
||||
await postText("Aborting current task.");
|
||||
},
|
||||
mute: async (commandInput: MuteCommandInput) => {
|
||||
const target = commandInput.target?.trim()
|
||||
@@ -931,11 +945,7 @@ export async function handleConnectorUserTurn<
|
||||
},
|
||||
{ timeoutMs: null },
|
||||
);
|
||||
await postConnectorText(
|
||||
input.thread,
|
||||
input.transport,
|
||||
"Steering current task.",
|
||||
);
|
||||
await postText("Steering current task.");
|
||||
return;
|
||||
}
|
||||
const sessionId = await getOrCreateSessionId({
|
||||
@@ -997,26 +1007,14 @@ export async function handleConnectorUserTurn<
|
||||
conversationId: input.thread.id,
|
||||
onToolStatus: async (message) => {
|
||||
if (toolStatusMessage) {
|
||||
toolStatusMessage = await editConnectorText(
|
||||
toolStatusMessage,
|
||||
input.transport,
|
||||
message,
|
||||
);
|
||||
toolStatusMessage = await editText(toolStatusMessage, message);
|
||||
return;
|
||||
}
|
||||
toolStatusMessage = await postConnectorText(
|
||||
input.thread,
|
||||
input.transport,
|
||||
message,
|
||||
);
|
||||
toolStatusMessage = await postText(message);
|
||||
},
|
||||
onApprovalRequested: async (approval) => {
|
||||
input.pendingApprovals.set(input.thread.id, approval);
|
||||
await postConnectorText(
|
||||
input.thread,
|
||||
input.transport,
|
||||
formatConnectorApprovalPrompt(approval),
|
||||
);
|
||||
await postText(formatConnectorApprovalPrompt(approval));
|
||||
},
|
||||
onCompleted: async (result) => {
|
||||
await input.onReplyCompleted?.({
|
||||
|
||||
@@ -12,6 +12,15 @@ const connectorDescriptions = new Map(
|
||||
);
|
||||
|
||||
const registry = new Map<string, ConnectorRegistryEntry>([
|
||||
[
|
||||
"agentphone",
|
||||
{
|
||||
name: "agentphone",
|
||||
description: connectorDescriptions.get("agentphone") ?? "AgentPhone",
|
||||
load: async () =>
|
||||
(await import("./adapters/agentphone")).agentPhoneConnector,
|
||||
},
|
||||
],
|
||||
[
|
||||
"discord",
|
||||
{
|
||||
|
||||
@@ -20,10 +20,15 @@ export type ActiveConnectorRecord = {
|
||||
pid: number;
|
||||
hubUrl: string;
|
||||
startedAt?: string;
|
||||
agentId?: string;
|
||||
applicationId?: string;
|
||||
botUsername?: string;
|
||||
userName?: string;
|
||||
agentPhoneNumber?: string;
|
||||
phoneNumberId?: string;
|
||||
phoneNumberCountry?: string;
|
||||
phoneNumberStatus?: string;
|
||||
phoneNumberType?: string;
|
||||
port?: number;
|
||||
baseUrl?: string;
|
||||
connectionMode?: string;
|
||||
@@ -72,18 +77,40 @@ const connectorFieldExtractors: Record<
|
||||
connectionMode: (p) =>
|
||||
typeof p.connectionMode === "string" ? p.connectionMode : undefined,
|
||||
userName: (p) => (typeof p.userName === "string" ? p.userName : undefined),
|
||||
agentPhoneNumber: (p) =>
|
||||
typeof p.agentPhoneNumber === "string" ? p.agentPhoneNumber : undefined,
|
||||
botUsername: (p) =>
|
||||
typeof p.botUsername === "string" ? p.botUsername : undefined,
|
||||
applicationId: (p) =>
|
||||
typeof p.applicationId === "string" ? p.applicationId : undefined,
|
||||
agentId: (p) => (typeof p.agentId === "string" ? p.agentId : undefined),
|
||||
phoneNumberId: (p) =>
|
||||
typeof p.phoneNumberId === "string" ? p.phoneNumberId : undefined,
|
||||
phoneNumberCountry: (p) =>
|
||||
typeof p.phoneNumberCountry === "string" ? p.phoneNumberCountry : undefined,
|
||||
phoneNumberStatus: (p) =>
|
||||
typeof p.phoneNumberStatus === "string" ? p.phoneNumberStatus : undefined,
|
||||
phoneNumberType: (p) =>
|
||||
typeof p.phoneNumberType === "string" ? p.phoneNumberType : undefined,
|
||||
};
|
||||
|
||||
const connectorConfigs: Record<
|
||||
string,
|
||||
{ required: ConnectorFieldKey[]; optional: ConnectorFieldKey[] }
|
||||
> = {
|
||||
agentphone: {
|
||||
required: ["userName", "agentId"],
|
||||
optional: [
|
||||
"startedAt",
|
||||
"port",
|
||||
"baseUrl",
|
||||
"agentPhoneNumber",
|
||||
"phoneNumberId",
|
||||
"phoneNumberCountry",
|
||||
"phoneNumberStatus",
|
||||
"phoneNumberType",
|
||||
],
|
||||
},
|
||||
discord: {
|
||||
required: ["userName", "applicationId"],
|
||||
optional: ["startedAt", "port", "baseUrl"],
|
||||
@@ -169,6 +196,7 @@ function readActiveConnectorRecord(
|
||||
|
||||
export function listActiveConnectors(): ActiveConnectorRecord[] {
|
||||
const connectorTypes: ActiveConnectorRecord["type"][] = [
|
||||
"agentphone",
|
||||
"discord",
|
||||
"telegram",
|
||||
"gchat",
|
||||
|
||||
@@ -10,9 +10,15 @@ describe("connect wizard platform security fields", () => {
|
||||
});
|
||||
|
||||
it("rejects unsafe Telegram and Slack access restriction identifiers", () => {
|
||||
const agentphone = PLATFORMS.find(
|
||||
(platform) => platform.id === "agentphone",
|
||||
);
|
||||
const telegram = PLATFORMS.find((platform) => platform.id === "telegram");
|
||||
const slack = PLATFORMS.find((platform) => platform.id === "slack");
|
||||
|
||||
const agentphoneParticipant = agentphone?.security?.fields.find(
|
||||
(field) => field.key === "participant",
|
||||
);
|
||||
const telegramUser = telegram?.security?.fields.find(
|
||||
(field) => field.key === "userId",
|
||||
);
|
||||
@@ -23,6 +29,13 @@ describe("connect wizard platform security fields", () => {
|
||||
(field) => field.key === "userId",
|
||||
);
|
||||
|
||||
expect(agentphoneParticipant?.validate?.("+15551234567")).toBeUndefined();
|
||||
expect(
|
||||
agentphoneParticipant?.validate?.("alice@example.com"),
|
||||
).toBeUndefined();
|
||||
expect(agentphoneParticipant?.validate?.("alice; rm -rf /")).toContain(
|
||||
"AgentPhone participant",
|
||||
);
|
||||
expect(telegramUser?.validate?.("123456")).toBeUndefined();
|
||||
expect(telegramUser?.validate?.("123; rm -rf /")).toContain("digits");
|
||||
expect(slackTeam?.validate?.("T01ABC123")).toBeUndefined();
|
||||
|
||||
@@ -75,7 +75,72 @@ function validateSlackUserId(value: string): string | undefined {
|
||||
: "Slack member ID must start with U or W and contain uppercase letters or digits only";
|
||||
}
|
||||
|
||||
function validateAgentPhoneParticipant(value: string): string | undefined {
|
||||
return /^[+@._a-zA-Z0-9-]+$/.test(value)
|
||||
? undefined
|
||||
: "AgentPhone participant must contain only letters, digits, +, @, ., _, or -";
|
||||
}
|
||||
|
||||
export const PLATFORMS: PlatformDef[] = [
|
||||
{
|
||||
id: "agentphone",
|
||||
name: "AgentPhone",
|
||||
type: "webhook",
|
||||
hint: "SMS, MMS, iMessage, and voice call transcripts.",
|
||||
fields: [
|
||||
{
|
||||
flag: "--api-key",
|
||||
label: "API key",
|
||||
required: true,
|
||||
help: ["Create or copy an AgentPhone API key"],
|
||||
},
|
||||
{
|
||||
flag: "--agent-id",
|
||||
label: "Agent ID",
|
||||
placeholder: "The AgentPhone agent ID to send messages from.",
|
||||
required: true,
|
||||
help: ["The AgentPhone agent ID to send messages from."],
|
||||
},
|
||||
{
|
||||
flag: "--base-url",
|
||||
label: "Cline Webhook URL",
|
||||
placeholder: "https://example.com",
|
||||
required: true,
|
||||
help: [
|
||||
"Your publicly accessible URL for webhook callbacks from Cline",
|
||||
"Set the Agent Webhook in AgentPhone's Agents setting to <base-url>/api/webhooks/agentphone",
|
||||
"Use the returned Signing Secret to configure the --webhook-secret field below",
|
||||
],
|
||||
},
|
||||
{
|
||||
flag: "--webhook-secret",
|
||||
label: "Webhook signing secret",
|
||||
required: true,
|
||||
help: [
|
||||
"The signing secret associated with the AgentPhone's Agent Webhook",
|
||||
],
|
||||
},
|
||||
],
|
||||
security: {
|
||||
prompt: "Restrict which AgentPhone sender can interact with the bot?",
|
||||
fields: [
|
||||
{
|
||||
key: "participant",
|
||||
label: "Allowed phone or email",
|
||||
placeholder: "+15551234567",
|
||||
help: [
|
||||
"Use the sender phone number or iMessage email address",
|
||||
"The value is matched against agentphone:user:<value>",
|
||||
],
|
||||
requiredMessage:
|
||||
"Phone number or email is required to restrict access",
|
||||
validate: validateAgentPhoneParticipant,
|
||||
},
|
||||
],
|
||||
buildHookCommand: ({ participant }) =>
|
||||
`jq -r ".payload.actor.participantKey" | grep -F -q "agentphone:user:${participant}" && echo '{"action":"allow"}' || echo '{"action":"deny"}'`,
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "telegram",
|
||||
name: "Telegram",
|
||||
|
||||
@@ -169,6 +169,45 @@ export async function startConnectorChannel(
|
||||
return connectorChannelsPayload();
|
||||
}
|
||||
|
||||
export async function updateConnectorChannel(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<WebviewConnectorChannelsResponse> {
|
||||
const cliArgs = buildConnectorStartArgs(args);
|
||||
const channel = cliArgs[0] ?? "";
|
||||
const stopResult = await runCliConnectCommand([channel, "--stop"]);
|
||||
if (stopResult.code !== 0) {
|
||||
throw new Error(
|
||||
(
|
||||
stopResult.stderr.trim() ||
|
||||
stopResult.stdout.trim() ||
|
||||
"connector stop failed"
|
||||
)
|
||||
.trim()
|
||||
.slice(0, 2_000),
|
||||
);
|
||||
}
|
||||
await waitForConnectorState(
|
||||
() =>
|
||||
!listActiveConnectors().some((connector) => connector.type === channel),
|
||||
);
|
||||
const startResult = await runCliConnectCommand(cliArgs);
|
||||
if (startResult.code !== 0) {
|
||||
throw new Error(
|
||||
(
|
||||
startResult.stderr.trim() ||
|
||||
startResult.stdout.trim() ||
|
||||
"connector update failed"
|
||||
)
|
||||
.trim()
|
||||
.slice(0, 2_000),
|
||||
);
|
||||
}
|
||||
await waitForConnectorState(() =>
|
||||
listActiveConnectors().some((connector) => connector.type === channel),
|
||||
);
|
||||
return connectorChannelsPayload();
|
||||
}
|
||||
|
||||
export async function stopConnectorChannel(
|
||||
args?: Record<string, unknown>,
|
||||
): Promise<WebviewConnectorChannelsResponse> {
|
||||
|
||||
@@ -25,6 +25,7 @@ import {
|
||||
connectorChannelsPayload,
|
||||
startConnectorChannel,
|
||||
stopConnectorChannel,
|
||||
updateConnectorChannel,
|
||||
} from "./connectors";
|
||||
import { providerSettingsManager, workspaceRoot } from "./deps";
|
||||
import {
|
||||
@@ -168,6 +169,11 @@ export async function handleDesktopCommand(
|
||||
broadcastHubState(ctx);
|
||||
return response;
|
||||
}
|
||||
if (command === "update_connector_channel") {
|
||||
const response = await updateConnectorChannel(args);
|
||||
broadcastHubState(ctx);
|
||||
return response;
|
||||
}
|
||||
if (command === "list_mcp_servers") {
|
||||
return readMcpServersResponse();
|
||||
}
|
||||
|
||||
@@ -169,10 +169,15 @@ export type WebviewActiveConnector = {
|
||||
pid: number;
|
||||
hubUrl: string;
|
||||
startedAt?: string;
|
||||
agentId?: string;
|
||||
applicationId?: string;
|
||||
botUsername?: string;
|
||||
userName?: string;
|
||||
agentPhoneNumber?: string;
|
||||
phoneNumberId?: string;
|
||||
phoneNumberCountry?: string;
|
||||
phoneNumberStatus?: string;
|
||||
phoneNumberType?: string;
|
||||
port?: number;
|
||||
baseUrl?: string;
|
||||
connectionMode?: string;
|
||||
|
||||
@@ -210,6 +210,9 @@ function connectorLabel(connector: WebviewActiveConnector): string {
|
||||
if (connector.botUsername) {
|
||||
return `@${connector.botUsername}`;
|
||||
}
|
||||
if (connector.agentPhoneNumber) {
|
||||
return connector.agentPhoneNumber;
|
||||
}
|
||||
if (connector.userName) {
|
||||
return connector.userName;
|
||||
}
|
||||
@@ -219,6 +222,17 @@ function connectorLabel(connector: WebviewActiveConnector): string {
|
||||
return shortId(connector.id);
|
||||
}
|
||||
|
||||
function connectorDetails(connector: WebviewActiveConnector): string {
|
||||
return [
|
||||
connectorLabel(connector),
|
||||
connector.agentId ? `agent=${connector.agentId}` : undefined,
|
||||
connector.phoneNumberType ? `type=${connector.phoneNumberType}` : undefined,
|
||||
`pid=${connector.pid}`,
|
||||
]
|
||||
.filter((detail): detail is string => Boolean(detail))
|
||||
.join(" | ");
|
||||
}
|
||||
|
||||
function sessionRunDetails(session: WebviewSessionSummary): string[] {
|
||||
const inputTokens = formatCompactNumber(session.inputTokens);
|
||||
const outputTokens = formatCompactNumber(session.outputTokens);
|
||||
@@ -534,7 +548,7 @@ function HomeView({
|
||||
className="block truncate text-[11px] text-muted-foreground"
|
||||
title={connector.hubUrl}
|
||||
>
|
||||
{connectorLabel(connector)} | pid={connector.pid}
|
||||
{connectorDetails(connector)}
|
||||
</span>
|
||||
</div>
|
||||
<span
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client";
|
||||
|
||||
import { Circle, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||
import { Circle, Pencil, Plus, RefreshCw, Trash2 } from "lucide-react";
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
import {
|
||||
AlertDialog,
|
||||
@@ -77,10 +77,15 @@ type ActiveConnector = {
|
||||
pid: number;
|
||||
hubUrl: string;
|
||||
startedAt?: string;
|
||||
agentId?: string;
|
||||
applicationId?: string;
|
||||
botUsername?: string;
|
||||
userName?: string;
|
||||
agentPhoneNumber?: string;
|
||||
phoneNumberId?: string;
|
||||
phoneNumberCountry?: string;
|
||||
phoneNumberStatus?: string;
|
||||
phoneNumberType?: string;
|
||||
port?: number;
|
||||
baseUrl?: string;
|
||||
connectionMode?: string;
|
||||
@@ -98,6 +103,8 @@ type ConnectorFormState = {
|
||||
securityValues: Record<string, string>;
|
||||
};
|
||||
|
||||
type ConnectorFormMode = "add" | "edit";
|
||||
|
||||
function connectorName(
|
||||
connector: ActiveConnector,
|
||||
channels: ConnectorChannel[],
|
||||
@@ -112,6 +119,9 @@ function connectorIdentity(connector: ActiveConnector): string {
|
||||
if (connector.botUsername) {
|
||||
return `@${connector.botUsername}`;
|
||||
}
|
||||
if (connector.agentPhoneNumber) {
|
||||
return connector.agentPhoneNumber;
|
||||
}
|
||||
if (connector.userName) {
|
||||
return connector.userName;
|
||||
}
|
||||
@@ -121,6 +131,30 @@ function connectorIdentity(connector: ActiveConnector): string {
|
||||
return `pid ${connector.pid}`;
|
||||
}
|
||||
|
||||
function connectorDetailBadges(connector: ActiveConnector): string[] {
|
||||
return [
|
||||
connector.agentId ? `agent=${connector.agentId}` : undefined,
|
||||
connector.applicationId ? `app=${connector.applicationId}` : undefined,
|
||||
connector.agentPhoneNumber
|
||||
? `phone=${connector.agentPhoneNumber}`
|
||||
: undefined,
|
||||
connector.phoneNumberType ? `type=${connector.phoneNumberType}` : undefined,
|
||||
connector.phoneNumberStatus
|
||||
? `status=${connector.phoneNumberStatus}`
|
||||
: undefined,
|
||||
connector.phoneNumberId ? `numberId=${connector.phoneNumberId}` : undefined,
|
||||
connector.port ? `port=${connector.port}` : undefined,
|
||||
`pid=${connector.pid}`,
|
||||
].filter((detail): detail is string => Boolean(detail));
|
||||
}
|
||||
|
||||
function connectorWebhookUrl(connector: ActiveConnector): string | undefined {
|
||||
if (!connector.baseUrl || connector.type === "telegram") {
|
||||
return undefined;
|
||||
}
|
||||
return `${connector.baseUrl.replace(/\/$/, "")}/api/webhooks/${connector.type}`;
|
||||
}
|
||||
|
||||
function formatDateTime(value?: string): string {
|
||||
if (!value) {
|
||||
return "-";
|
||||
@@ -190,6 +224,50 @@ function createFormState(channels: ConnectorChannel[]): ConnectorFormState {
|
||||
};
|
||||
}
|
||||
|
||||
function connectorFieldValue(
|
||||
connector: ActiveConnector,
|
||||
flag: string,
|
||||
): string | undefined {
|
||||
if (flag === "--agent-id") {
|
||||
return connector.agentId;
|
||||
}
|
||||
if (flag === "--application-id") {
|
||||
return connector.applicationId;
|
||||
}
|
||||
if (flag === "--phone-number-id") {
|
||||
return connector.phoneNumberId;
|
||||
}
|
||||
if (flag === "--base-url") {
|
||||
return connector.baseUrl;
|
||||
}
|
||||
if (flag === "--user-name") {
|
||||
return connector.userName;
|
||||
}
|
||||
if (flag === "--bot-username") {
|
||||
return connector.botUsername;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function createEditFormState(
|
||||
connector: ActiveConnector,
|
||||
channel: ConnectorChannel,
|
||||
): ConnectorFormState {
|
||||
const values: Record<string, string> = {};
|
||||
for (const field of channel.fields) {
|
||||
const value = connectorFieldValue(connector, field.flag);
|
||||
if (value) {
|
||||
values[field.flag] = value;
|
||||
}
|
||||
}
|
||||
return {
|
||||
channelId: channel.id,
|
||||
values,
|
||||
securityEnabled: false,
|
||||
securityValues: {},
|
||||
};
|
||||
}
|
||||
|
||||
export function ChannelsContent() {
|
||||
const [channels, setChannels] = useState<ConnectorChannel[]>([]);
|
||||
const [activeConnectors, setActiveConnectors] = useState<ActiveConnector[]>(
|
||||
@@ -199,6 +277,8 @@ export function ChannelsContent() {
|
||||
const [busyChannel, setBusyChannel] = useState<string | null>(null);
|
||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||
const [dialogOpen, setDialogOpen] = useState(false);
|
||||
const [formMode, setFormMode] = useState<ConnectorFormMode>("add");
|
||||
const [editTarget, setEditTarget] = useState<ActiveConnector | null>(null);
|
||||
const [formState, setFormState] = useState<ConnectorFormState>({
|
||||
channelId: "",
|
||||
values: {},
|
||||
@@ -256,11 +336,26 @@ export function ChannelsContent() {
|
||||
}, [refreshChannels]);
|
||||
|
||||
const openAddDialog = () => {
|
||||
setFormMode("add");
|
||||
setEditTarget(null);
|
||||
setFormState(createFormState(channels));
|
||||
setFormError(null);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const openEditDialog = (connector: ActiveConnector) => {
|
||||
const channel = channels.find((entry) => entry.id === connector.type);
|
||||
if (!channel) {
|
||||
setErrorMessage(`Unknown connector channel: ${connector.type}`);
|
||||
return;
|
||||
}
|
||||
setFormMode("edit");
|
||||
setEditTarget(connector);
|
||||
setFormState(createEditFormState(connector, channel));
|
||||
setFormError(null);
|
||||
setDialogOpen(true);
|
||||
};
|
||||
|
||||
const updateFieldValue = (flag: string, value: string) => {
|
||||
setFormState((prev) => ({
|
||||
...prev,
|
||||
@@ -275,7 +370,7 @@ export function ChannelsContent() {
|
||||
}));
|
||||
};
|
||||
|
||||
const startConnector = async () => {
|
||||
const saveConnector = async () => {
|
||||
if (!selectedChannel) {
|
||||
setFormError("Choose a channel");
|
||||
return;
|
||||
@@ -302,9 +397,12 @@ export function ChannelsContent() {
|
||||
setErrorMessage(null);
|
||||
try {
|
||||
const response = await desktopClient.invoke<ConnectorChannelsResponse>(
|
||||
"start_connector_channel",
|
||||
formMode === "edit"
|
||||
? "update_connector_channel"
|
||||
: "start_connector_channel",
|
||||
{
|
||||
channel: selectedChannel.id,
|
||||
connectorId: editTarget?.id,
|
||||
values: formState.values,
|
||||
security: {
|
||||
enabled: formState.securityEnabled,
|
||||
@@ -314,6 +412,8 @@ export function ChannelsContent() {
|
||||
);
|
||||
applyResponse(response);
|
||||
setDialogOpen(false);
|
||||
setEditTarget(null);
|
||||
setFormMode("add");
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
setFormError(message);
|
||||
@@ -407,21 +507,34 @@ export function ChannelsContent() {
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 flex flex-wrap gap-1.5 text-[11px] text-muted-foreground">
|
||||
<span className="rounded-md border bg-background px-1.5 py-0.5">
|
||||
pid={connector.pid}
|
||||
</span>
|
||||
{connectorDetailBadges(connector).map((detail) => (
|
||||
<span
|
||||
className="rounded-md border bg-background px-1.5 py-0.5"
|
||||
key={detail}
|
||||
>
|
||||
{detail}
|
||||
</span>
|
||||
))}
|
||||
<span
|
||||
className="max-w-full break-all rounded-md border bg-background px-1.5 py-0.5"
|
||||
title={connector.hubUrl}
|
||||
>
|
||||
{connector.hubUrl}
|
||||
hub={connector.hubUrl}
|
||||
</span>
|
||||
{connector.baseUrl ? (
|
||||
<span
|
||||
className="max-w-full break-all rounded-md border bg-background px-1.5 py-0.5"
|
||||
title={connector.baseUrl}
|
||||
>
|
||||
{connector.baseUrl}
|
||||
base={connector.baseUrl}
|
||||
</span>
|
||||
) : null}
|
||||
{connectorWebhookUrl(connector) ? (
|
||||
<span
|
||||
className="max-w-full break-all rounded-md border bg-background px-1.5 py-0.5"
|
||||
title={connectorWebhookUrl(connector)}
|
||||
>
|
||||
webhook={connectorWebhookUrl(connector)}
|
||||
</span>
|
||||
) : null}
|
||||
<span className="rounded-md border bg-background px-1.5 py-0.5">
|
||||
@@ -434,16 +547,26 @@ export function ChannelsContent() {
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<Button
|
||||
disabled={busyChannel === connector.type}
|
||||
onClick={() => setRemoveTarget(connector)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
Remove...
|
||||
</Button>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
disabled={busyChannel === connector.type}
|
||||
onClick={() => openEditDialog(connector)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Pencil className="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
disabled={busyChannel === connector.type}
|
||||
onClick={() => setRemoveTarget(connector)}
|
||||
size="sm"
|
||||
type="button"
|
||||
variant="outline"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
@@ -451,18 +574,32 @@ export function ChannelsContent() {
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>
|
||||
<Dialog
|
||||
open={dialogOpen}
|
||||
onOpenChange={(open: boolean) => {
|
||||
setDialogOpen(open);
|
||||
if (!open) {
|
||||
setEditTarget(null);
|
||||
setFormMode("add");
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent className="max-h-[86vh] overflow-y-auto sm:max-w-xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>Add Channel</DialogTitle>
|
||||
<DialogTitle>
|
||||
{formMode === "edit" ? "Edit Channel" : "Add Channel"}
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
Start a connector channel for Cline Hub.
|
||||
{formMode === "edit"
|
||||
? "Update this connector channel and restart it with the new settings."
|
||||
: "Start a connector channel for Cline Hub."}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="grid gap-4 py-2">
|
||||
<div className="grid gap-2">
|
||||
<Label>Channel</Label>
|
||||
<Select
|
||||
disabled={formMode === "edit"}
|
||||
onValueChange={(value) => {
|
||||
if (!value) {
|
||||
return;
|
||||
@@ -491,6 +628,13 @@ export function ChannelsContent() {
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{formMode === "edit" ? (
|
||||
<div className="rounded-md border bg-muted/40 px-3 py-2 text-xs text-muted-foreground">
|
||||
Stored runtime fields are prefilled. Secret fields are not read
|
||||
back from the running connector, so re-enter them before saving.
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
{visibleFields.map((field) => (
|
||||
<div className="grid gap-2" key={field.flag}>
|
||||
<Label>
|
||||
@@ -540,6 +684,13 @@ export function ChannelsContent() {
|
||||
value={formState.values[field.flag] ?? ""}
|
||||
/>
|
||||
)}
|
||||
{field.help?.length ? (
|
||||
<div className="grid gap-1 text-xs text-muted-foreground">
|
||||
{field.help.map((line) => (
|
||||
<p key={line}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -572,6 +723,13 @@ export function ChannelsContent() {
|
||||
type={isSecretField(field) ? "password" : "text"}
|
||||
value={formState.securityValues[field.key] ?? ""}
|
||||
/>
|
||||
{field.help?.length ? (
|
||||
<div className="grid gap-1 text-xs text-muted-foreground">
|
||||
{field.help.map((line) => (
|
||||
<p key={line}>{line}</p>
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
))
|
||||
: null}
|
||||
@@ -595,10 +753,16 @@ export function ChannelsContent() {
|
||||
</Button>
|
||||
<Button
|
||||
disabled={busyChannel !== null || !selectedChannel}
|
||||
onClick={() => void startConnector()}
|
||||
onClick={() => void saveConnector()}
|
||||
type="button"
|
||||
>
|
||||
{busyChannel ? "Starting..." : "Add Channel"}
|
||||
{busyChannel
|
||||
? formMode === "edit"
|
||||
? "Saving..."
|
||||
: "Starting..."
|
||||
: formMode === "edit"
|
||||
? "Save Changes"
|
||||
: "Add Channel"}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@agentclientprotocol/sdk": "^0.16.1",
|
||||
"@agentphone/chat-sdk-adapter": "^0.1.0",
|
||||
"@chat-adapter/discord": "^4.23.0",
|
||||
"@chat-adapter/gchat": "^4.23.0",
|
||||
"@chat-adapter/linear": "^4.23.0",
|
||||
@@ -37,7 +38,7 @@
|
||||
"@opentui-ui/dialog": "^0.1.2",
|
||||
"@opentui/core": "0.1.102",
|
||||
"@opentui/react": "0.1.102",
|
||||
"chat": "^4.23.0",
|
||||
"chat": "^4.30.0",
|
||||
"commander": "^14.0.3",
|
||||
"fzf": "^0.5.2",
|
||||
"marked": "^15.0.12",
|
||||
@@ -464,6 +465,8 @@
|
||||
"packages": {
|
||||
"@agentclientprotocol/sdk": ["@agentclientprotocol/sdk@0.16.1", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, "sha512-1ad+Sc/0sCtZGHthxxvgEUo5Wsbw16I+aF+YwdiLnPwkZG8KAGUEAPK6LM6Pf69lCyJPt1Aomk1d+8oE3C4ZEw=="],
|
||||
|
||||
"@agentphone/chat-sdk-adapter": ["@agentphone/chat-sdk-adapter@0.1.0", "", { "dependencies": { "@chat-adapter/shared": "^4.20.0" }, "peerDependencies": { "chat": "^4.20.0" } }, "sha512-cal0EcWdbROsBT2cFd4TPxDM6SggdxifOkGCI/YFrmWapXSUgdA3wXTsZ3C8Z9Pcs1Zlg8ShNf2JSOqkvfHIWw=="],
|
||||
|
||||
"@ai-sdk/amazon-bedrock": ["@ai-sdk/amazon-bedrock@4.0.111", "", { "dependencies": { "@ai-sdk/anthropic": "3.0.81", "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27", "@smithy/eventstream-codec": "^4.0.1", "@smithy/util-utf8": "^4.0.0", "aws4fetch": "^1.0.20" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-cH71k96tcnLuq1x3xi0KP384Jxio8qM6VQzHDUfU4OuX2P83FC/pBvksR5YVRm17GdQliAfR1t5o6z1iJRtfpA=="],
|
||||
|
||||
"@ai-sdk/anthropic": ["@ai-sdk/anthropic@3.0.81", "", { "dependencies": { "@ai-sdk/provider": "3.0.10", "@ai-sdk/provider-utils": "4.0.27" }, "peerDependencies": { "zod": "^3.25.76 || ^4.1.8" } }, "sha512-B1JDd9Ugq9R5AgIaW3674lhGCMMYJcPUxnrZh8fzbGojgg4QvHFRv6eZahGQAUsmGHbcf74G9bdSBDLWQGY2GA=="],
|
||||
@@ -520,8 +523,6 @@
|
||||
|
||||
"@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="],
|
||||
|
||||
"@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.1058.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.48", "@aws-sdk/eventstream-handler-node": "^3.972.18", "@aws-sdk/middleware-eventstream": "^3.972.14", "@aws-sdk/middleware-websocket": "^3.972.23", "@aws-sdk/token-providers": "3.1058.0", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-p2co5KPnRLByP8VwLJIdNM7TFwa68ZXZJjPWCovIHsJ8L/oN83Oze4/gv3UHO+fKYI4Ve3M3N3KKSH6aTsXTSw=="],
|
||||
|
||||
"@aws-sdk/client-cognito-identity": ["@aws-sdk/client-cognito-identity@3.1056.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-node": "^3.972.46", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Fywg6+B39uGiYZRYFEsOXbIeHQ8wvtMqlt6FUwWev8N2H+V0pVdgCKn32pSOzud1i17wnm5gpB2VXZEoyVHc2A=="],
|
||||
|
||||
"@aws-sdk/core": ["@aws-sdk/core@3.974.15", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@aws-sdk/xml-builder": "^3.972.26", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/core": "^3.24.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-UpA0rTGW/tHGITcCqHisbuuEPraYg9GG+mWmXjY5+RxZBMLGe6aL9oe0ix50LztwAcPIkGZLH0yWdMIkCM10hw=="],
|
||||
@@ -546,17 +547,11 @@
|
||||
|
||||
"@aws-sdk/credential-providers": ["@aws-sdk/credential-providers@3.1056.0", "", { "dependencies": { "@aws-sdk/client-cognito-identity": "3.1056.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-cognito-identity": "^3.972.38", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.45", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-node": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-Qp7ndCG+dZldiaURze6BM/dLkHQJxwi6WNRR1sR9lhX9jS9QG5ZIOiY3jm6T668vgGqHuNQS7r/P9pimxnHyyg=="],
|
||||
|
||||
"@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.18", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-QPQhwY/fstR8fMZFWrsJRNoTP6D1RjRPHGRX7u9/VkF3opCsvD0oXPz6qzkX94SchzvuS5vyFZbJbPcMEs2Jeg=="],
|
||||
|
||||
"@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.14", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-DoZ4djVj/74XQ6M/IwxuKh543tTvLCL7u1Dx+VDHMgW9yGNrFSJJ1l0LrUQRaekic5CB12wUiiOoHL0VI6H0gg=="],
|
||||
|
||||
"@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.23", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-F0d4A9pJFiwljyKgSwU1Z5n+CXSv8bp+V5SthbS2rftB8wBN9z1K2Yyv3xbeK0AM2T0g4q6Ptf0shFF+oQZyiA=="],
|
||||
|
||||
"@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.997.13", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.974.15", "@aws-sdk/signature-v4-multi-region": "^3.996.30", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/fetch-http-handler": "^5.4.5", "@smithy/node-http-handler": "^4.7.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-2pA6eyb5nSo/ZD2cayhOTEMoGQYgspq0RI05GDLkzQ3ajZ6isS6waV6E92Am/hz4LIlLUTrbwPLurJ/fuiHvkg=="],
|
||||
|
||||
"@aws-sdk/signature-v4-multi-region": ["@aws-sdk/signature-v4-multi-region@3.996.30", "", { "dependencies": { "@aws-sdk/types": "^3.973.9", "@smithy/signature-v4": "^5.4.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-HULDLMVzkmTSEv6//7kx2kRevp/VYUpm8hJNNFbmhxDn0fUiGTxVcM9yg31TukvTq8nyOBDUN2gH0o5IRbKjdw=="],
|
||||
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1058.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-m22Usagf/HzN8Nlktz3Lt/IukBZl/JYBkhlpMHowXl2xtcXpcGuh59vtltC1Uy26pR86Ez2EqwVLl8eOZP6v3A=="],
|
||||
"@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1056.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA=="],
|
||||
|
||||
"@aws-sdk/types": ["@aws-sdk/types@3.973.9", "", { "dependencies": { "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-kuBfgQVdcz5Bmapc4A13YbpVw/pXkesfhetcFYwbntqas8sF41OHyd4o28+/TG2ZQdHBsv90Lsu5y6oitvYCdg=="],
|
||||
|
||||
@@ -1794,7 +1789,7 @@
|
||||
|
||||
"character-reference-invalid": ["character-reference-invalid@2.0.1", "", {}, "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw=="],
|
||||
|
||||
"chat": ["chat@4.29.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "zod"] }, "sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw=="],
|
||||
"chat": ["chat@4.30.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "zod"] }, "sha512-8LXrauKckMmR83FcYC/R8nNEda5VJDDdIhZwUUu+hzaSbk4lqsro0IWm7rB1GGYXONRrUOG2XJlkNr4C15vgMA=="],
|
||||
|
||||
"ci-info": ["ci-info@3.9.0", "", {}, "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ=="],
|
||||
|
||||
@@ -3284,12 +3279,22 @@
|
||||
|
||||
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
|
||||
|
||||
"@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.48", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-ini": "^3.972.46", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-QIbtJP0olSLZ2ImEu636pP+7JJbPfaL3xSJIFXhu472CWuondCc4bGOa8OeyhOFet8z4H1D/ZFKXc39FboWwYA=="],
|
||||
|
||||
"@aws-sdk/credential-provider-sso/@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.1056.0", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-81duvlltQlsfn5K+o8zILcystBRdbT1G2JJYVCML5NZHBz4CL/zf+sAemCtBh/uh6RQUMyInGeZLQ7/8igZhbA=="],
|
||||
|
||||
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
|
||||
|
||||
"@chat-adapter/discord/chat": ["chat@4.29.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "zod"] }, "sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw=="],
|
||||
|
||||
"@chat-adapter/gchat/chat": ["chat@4.29.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "zod"] }, "sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw=="],
|
||||
|
||||
"@chat-adapter/linear/chat": ["chat@4.29.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "zod"] }, "sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw=="],
|
||||
|
||||
"@chat-adapter/shared/chat": ["chat@4.29.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "zod"] }, "sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw=="],
|
||||
|
||||
"@chat-adapter/slack/chat": ["chat@4.29.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "zod"] }, "sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw=="],
|
||||
|
||||
"@chat-adapter/telegram/chat": ["chat@4.29.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "zod"] }, "sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw=="],
|
||||
|
||||
"@chat-adapter/whatsapp/chat": ["chat@4.29.0", "", { "dependencies": { "@workflow/serde": "4.1.0-beta.2", "mdast-util-to-string": "^4.0.0", "remark-gfm": "^4.0.0", "remark-parse": "^11.0.0", "remark-stringify": "^11.0.0", "remend": "^1.2.1", "unified": "^11.0.5" }, "peerDependencies": { "ai": "^6.0.182", "zod": "^3.0.0 || ^4.0.0" }, "optionalPeers": ["ai", "zod"] }, "sha512-KdPfzaie5ivYytyRICTERg5xT+LeCbYefokvNAqTHe92eqkFaoTMXXkSitikxJVWhZIb2YoXF1b9UZHyzSzKzw=="],
|
||||
|
||||
"@cline/cline-hub-webview/@types/node": ["@types/node@24.12.4", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-GUUEShf+PBCGW2KaXwcIt3Yk+e3pkKwWKb9GSyM9WQVE+ep2jzmHdGsHzu4wgcZy5fN9FBdVzjpBQsYlpfpgLA=="],
|
||||
|
||||
"@cline/code/lucide-react": ["lucide-react@0.564.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0" } }, "sha512-JJ8GVTQqFwuliifD48U6+h7DXEHdkhJ/E87kksGByII3qHxtPciVb8T8woQONHBQgHVOl7rSMrrip3SeVNy7Fg=="],
|
||||
@@ -3696,8 +3701,6 @@
|
||||
|
||||
"yargs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
|
||||
|
||||
"@aws-sdk/client-bedrock-runtime/@aws-sdk/credential-provider-node/@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.46", "", { "dependencies": { "@aws-sdk/core": "^3.974.15", "@aws-sdk/credential-provider-env": "^3.972.41", "@aws-sdk/credential-provider-http": "^3.972.43", "@aws-sdk/credential-provider-login": "^3.972.45", "@aws-sdk/credential-provider-process": "^3.972.41", "@aws-sdk/credential-provider-sso": "^3.972.45", "@aws-sdk/credential-provider-web-identity": "^3.972.45", "@aws-sdk/nested-clients": "^3.997.13", "@aws-sdk/types": "^3.973.9", "@smithy/core": "^3.24.5", "@smithy/credential-provider-imds": "^4.3.6", "@smithy/types": "^4.14.2", "tslib": "^2.6.2" } }, "sha512-hvcgcwOiS0nb2XFb5Op1Pz/vYaWz5K8kKullziGpdNRuG0NwzRXseuPt2CoBqknHGaSPVesu1aOn2OcctEYdCA=="],
|
||||
|
||||
"@cline/cline-hub-webview/@types/node/undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="],
|
||||
|
||||
"@dotenvx/dotenvx/execa/get-stream": ["get-stream@6.0.1", "", {}, "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="],
|
||||
|
||||
+37
-1
@@ -1,7 +1,7 @@
|
||||
---
|
||||
title: "Connectors"
|
||||
sidebarTitle: "Connectors"
|
||||
description: "Connect the CLI to Telegram, Slack, Discord, Google Chat, WhatsApp, etc."
|
||||
description: "Connect the CLI to AgentPhone, Telegram, Slack, Discord, Google Chat, WhatsApp, etc."
|
||||
---
|
||||
<Warning>
|
||||
This feature currently only applies to Cline CLI.
|
||||
@@ -21,6 +21,7 @@ cline connect
|
||||
|
||||
| Platform | Direct Command | Required Credentials |
|
||||
|----------|---------------|---------------------|
|
||||
| AgentPhone | `cline connect agentphone` | API key, agent ID, webhook signing secret, base URL |
|
||||
| Telegram | `cline connect telegram` | Bot token |
|
||||
| Slack | `cline connect slack` | Bot token plus webhook signing secret/base URL or socket app token |
|
||||
| Discord | `cline connect discord` | Application ID, bot token, public key, base URL |
|
||||
@@ -73,6 +74,41 @@ By default, anyone who finds your bot can message it and it will execute tasks o
|
||||
|
||||
The `--hook-command` receives each incoming message with sender info via stdin. Your script returns `{"action": "allow"}` or `{"action": "deny", "message": "reason"}`. Without `--hook-command`, everything is auto-approved.
|
||||
|
||||
## AgentPhone
|
||||
|
||||
Requires an AgentPhone API key, agent ID, webhook signing secret, and public base URL. Startup verifies the API key by calling AgentPhone's `/v1/numbers` endpoint and stores the active phone number assigned to the agent.
|
||||
|
||||
```bash
|
||||
cline connect agentphone \
|
||||
--api-key <KEY> \
|
||||
--agent-id <AGENT-ID> \
|
||||
--webhook-secret <SECRET> \
|
||||
--base-url <URL>
|
||||
```
|
||||
|
||||
AgentPhone handles SMS, MMS, iMessage, and completed voice call transcripts through one webhook.
|
||||
Configure the webhook URL in AgentPhone as:
|
||||
|
||||
```text
|
||||
<base-url>/api/webhooks/agentphone
|
||||
```
|
||||
|
||||
Inbound messages and completed voice call transcripts map to Cline sessions by sender identity, so the same phone number or iMessage address continues the same agent session.
|
||||
For live voice webhook requests, the connector returns a JSON spoken acknowledgment immediately; Cline processes the transcript event after the call ends.
|
||||
|
||||
### AgentPhone Security
|
||||
|
||||
By default, anyone who can message your AgentPhone number can ask it to run tasks. Restrict access with `--hook-command`. The hook receives participant keys such as `agentphone:user:+15551234567`.
|
||||
|
||||
```bash
|
||||
cline connect agentphone \
|
||||
--api-key <KEY> \
|
||||
--agent-id <AGENT-ID> \
|
||||
--webhook-secret <SECRET> \
|
||||
--base-url <URL> \
|
||||
--hook-command 'jq -r ".payload.actor.participantKey" | grep -F -q "agentphone:user:+15551234567" && echo "{\"action\":\"allow\"}" || echo "{\"action\":\"deny\",\"message\":\"unauthorized\"}"'
|
||||
```
|
||||
|
||||
## Slack
|
||||
|
||||
Slack supports webhook mode and socket mode. Each Slack thread maps to an agent session, so the agent maintains conversation context within a thread.
|
||||
|
||||
@@ -32,6 +32,44 @@ export type WhatsAppConnectorState = {
|
||||
startedAt: string;
|
||||
};
|
||||
|
||||
export type ConnectAgentPhoneOptions = {
|
||||
userName?: string;
|
||||
apiKey: string;
|
||||
agentId: string;
|
||||
webhookSecret?: string;
|
||||
apiUrl?: string;
|
||||
cwd: string;
|
||||
model?: string;
|
||||
provider?: string;
|
||||
apiProviderKey?: string;
|
||||
systemPrompt?: string;
|
||||
mode: "act" | "plan";
|
||||
interactive: boolean;
|
||||
maxIterations?: number;
|
||||
enableTools: boolean;
|
||||
rpcAddress: string;
|
||||
hookCommand?: string;
|
||||
port: number;
|
||||
host: string;
|
||||
baseUrl: string;
|
||||
};
|
||||
|
||||
export type AgentPhoneConnectorState = {
|
||||
instanceKey: string;
|
||||
userName: string;
|
||||
agentId: string;
|
||||
agentPhoneNumber: string;
|
||||
phoneNumberId: string;
|
||||
phoneNumberCountry?: string;
|
||||
phoneNumberStatus?: string;
|
||||
phoneNumberType?: string;
|
||||
pid: number;
|
||||
rpcAddress: string;
|
||||
port: number;
|
||||
baseUrl: string;
|
||||
startedAt: string;
|
||||
};
|
||||
|
||||
export type ConnectTelegramOptions = {
|
||||
botToken: string;
|
||||
botUsername?: string;
|
||||
|
||||
Reference in New Issue
Block a user