@clickonsearch/linkedin-agent 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -0
- package/dist/agui.js +89 -0
- package/dist/agui.js.map +1 -0
- package/dist/cli.js +56 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/providers.js +112 -0
- package/dist/providers.js.map +1 -0
- package/dist/server-cli.js +36 -0
- package/dist/server-cli.js.map +1 -0
- package/dist/server.js +49 -0
- package/dist/server.js.map +1 -0
- package/package.json +53 -0
- package/public/index.html +314 -0
package/README.md
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
# linkedin-agent
|
|
2
|
+
|
|
3
|
+
An agent for your personal LinkedIn account — read your own profile, publish
|
|
4
|
+
posts to your feed. Same shape as [`github-agent`](../github-agent): a
|
|
5
|
+
one-shot CLI and an AG-UI browser chat UI, both routed through whichever
|
|
6
|
+
provider you pick (OpenAI, Claude, or DeepSeek).
|
|
7
|
+
|
|
8
|
+
Requires [`@clickonsearch/linkedin-mcp-server`](../../mcp-servers/linkedin-mcp-server)
|
|
9
|
+
running (and already authorized — see that package's README) first.
|
|
10
|
+
|
|
11
|
+
## Known limits, inherited from linkedin-mcp-server
|
|
12
|
+
|
|
13
|
+
This agent only has two real capabilities: reading your own profile and
|
|
14
|
+
posting to your own feed. It **cannot** send LinkedIn messages (no public
|
|
15
|
+
API for that, at all) or read/reply to other people's posts (needs a
|
|
16
|
+
LinkedIn Developer Program approval most apps don't have). If you ask it to
|
|
17
|
+
do either, it's instructed to say so rather than pretend.
|
|
18
|
+
|
|
19
|
+
## Setup
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
# 1. make sure linkedin-mcp-server is running and authorized (separate terminal)
|
|
23
|
+
cd mcp-servers/linkedin-mcp-server
|
|
24
|
+
npm install && cp .env.example .env # fill in LINKEDIN_CLIENT_ID/SECRET
|
|
25
|
+
npm run dev:authorize # one-time OAuth consent
|
|
26
|
+
npm run dev
|
|
27
|
+
|
|
28
|
+
# 2. then this agent
|
|
29
|
+
cd agents/linkedin-agent
|
|
30
|
+
npm install
|
|
31
|
+
cp .env.example .env
|
|
32
|
+
# fill in LINKEDIN_MCP_URL (if not localhost:4300) and your provider's API key
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
## Run: CLI
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
npm run dev -- --provider claude "what does my profile look like?"
|
|
39
|
+
npm run dev -- --provider openai "post: excited to share our new release today"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
or build and run the compiled CLI:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
npm run build
|
|
46
|
+
npm start -- --provider claude "your prompt here"
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
## Run: AG-UI server + chat UI
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
npm run build
|
|
53
|
+
npm run serve # or `npm run dev:serve` for tsx, no build step
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
This starts an HTTP server (default `http://localhost:3200`) with:
|
|
57
|
+
|
|
58
|
+
- `GET /` — a self-contained chat page (`public/index.html`) with a provider dropdown.
|
|
59
|
+
- `POST /agent` — the AG-UI protocol endpoint, same shape as `github-agent`'s.
|
|
60
|
+
- `GET /health` — liveness check.
|
|
61
|
+
|
|
62
|
+
## Config
|
|
63
|
+
|
|
64
|
+
| Env var | Required | Notes |
|
|
65
|
+
| -------------------- | ----------------------- | ----------------------------------------------------- |
|
|
66
|
+
| `LINKEDIN_MCP_URL` | no (default `http://localhost:4300`) | where linkedin-mcp-server is running |
|
|
67
|
+
| `PORT` | no (default `3200`) | AG-UI server only |
|
|
68
|
+
| `PROVIDER` | no (default `openai`) | CLI: overridden by `--provider`. Server: default only, overridden per-request by `forwardedProps.provider` |
|
|
69
|
+
| `OPENAI_API_KEY` | when provider=openai | |
|
|
70
|
+
| `OPENAI_MODEL` | no (default `gpt-4.1`) | |
|
|
71
|
+
| `ANTHROPIC_API_KEY` | when provider=claude | |
|
|
72
|
+
| `ANTHROPIC_MODEL` | no (default `claude-sonnet-5`) | |
|
|
73
|
+
| `DEEPSEEK_API_KEY` | when provider=deepseek | |
|
|
74
|
+
| `DEEPSEEK_MODEL` | no (default `deepseek-chat`) | |
|
package/dist/agui.js
ADDED
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { EventType, contentToText } from "@ag-ui/core";
|
|
3
|
+
import { EventEncoder } from "@ag-ui/encoder";
|
|
4
|
+
import { runLinkedInAgent, isProvider } from "./providers.js";
|
|
5
|
+
/**
|
|
6
|
+
* Mirrors agents/github-agent's and agents/whatsapp-agent's agui.ts. The
|
|
7
|
+
* agent loop only takes a single prompt string, not a structured history,
|
|
8
|
+
* so every prior user/assistant message is flattened into a text
|
|
9
|
+
* transcript passed as a system preamble, and the most recent user message
|
|
10
|
+
* becomes the prompt.
|
|
11
|
+
*/
|
|
12
|
+
function extractPrompt(messages) {
|
|
13
|
+
const lastUserIndex = messages.map((m) => m.role).lastIndexOf("user");
|
|
14
|
+
if (lastUserIndex === -1) {
|
|
15
|
+
throw new Error("RunAgentInput.messages must include at least one user message");
|
|
16
|
+
}
|
|
17
|
+
const lastUser = messages[lastUserIndex];
|
|
18
|
+
const userPrompt = contentToText(lastUser.content);
|
|
19
|
+
const transcript = messages
|
|
20
|
+
.slice(0, lastUserIndex)
|
|
21
|
+
.filter((m) => m.role === "user" || m.role === "assistant")
|
|
22
|
+
.map((m) => `${m.role}: ${m.role === "assistant" ? (m.content ?? "") : contentToText(m.content)}`)
|
|
23
|
+
.filter((line) => line.length > "user: ".length || line.length > "assistant: ".length)
|
|
24
|
+
.join("\n");
|
|
25
|
+
return {
|
|
26
|
+
systemPrompt: transcript ? `Prior conversation for context:\n${transcript}` : undefined,
|
|
27
|
+
userPrompt,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Runs the LinkedIn agent for one AG-UI `RunAgentInput`, streaming the
|
|
32
|
+
* result to `res` as AG-UI protocol events (SSE by default).
|
|
33
|
+
*/
|
|
34
|
+
export async function runAgUiRequest(params) {
|
|
35
|
+
const { input, linkedinMcpUrl, defaultProvider, res, acceptHeader } = params;
|
|
36
|
+
const encoder = new EventEncoder({ accept: acceptHeader });
|
|
37
|
+
res.writeHead(200, {
|
|
38
|
+
"Content-Type": encoder.getContentType(),
|
|
39
|
+
"Cache-Control": "no-cache, no-transform",
|
|
40
|
+
Connection: "keep-alive",
|
|
41
|
+
"X-Accel-Buffering": "no",
|
|
42
|
+
});
|
|
43
|
+
const emit = (event) => {
|
|
44
|
+
res.write(encoder.encode(event));
|
|
45
|
+
};
|
|
46
|
+
const threadId = input.threadId ?? randomUUID();
|
|
47
|
+
const runId = input.runId ?? randomUUID();
|
|
48
|
+
emit({ type: EventType.RUN_STARTED, threadId, runId });
|
|
49
|
+
try {
|
|
50
|
+
const provider = isProvider(input.forwardedProps?.provider) ? input.forwardedProps.provider : defaultProvider;
|
|
51
|
+
const { systemPrompt, userPrompt } = extractPrompt(input.messages);
|
|
52
|
+
await runLinkedInAgent(provider, {
|
|
53
|
+
prompt: userPrompt,
|
|
54
|
+
systemPrompt,
|
|
55
|
+
linkedinMcpUrl,
|
|
56
|
+
onAssistantText: (text) => {
|
|
57
|
+
const messageId = randomUUID();
|
|
58
|
+
emit({ type: EventType.TEXT_MESSAGE_START, messageId, role: "assistant" });
|
|
59
|
+
emit({ type: EventType.TEXT_MESSAGE_CONTENT, messageId, delta: text });
|
|
60
|
+
emit({ type: EventType.TEXT_MESSAGE_END, messageId });
|
|
61
|
+
},
|
|
62
|
+
onToolCall: (name, args, toolCallId) => {
|
|
63
|
+
emit({ type: EventType.TOOL_CALL_START, toolCallId, toolCallName: name });
|
|
64
|
+
emit({ type: EventType.TOOL_CALL_ARGS, toolCallId, delta: JSON.stringify(args ?? {}) });
|
|
65
|
+
emit({ type: EventType.TOOL_CALL_END, toolCallId });
|
|
66
|
+
},
|
|
67
|
+
onToolResult: (toolCallId, _name, result) => {
|
|
68
|
+
emit({
|
|
69
|
+
type: EventType.TOOL_CALL_RESULT,
|
|
70
|
+
messageId: randomUUID(),
|
|
71
|
+
toolCallId,
|
|
72
|
+
content: result,
|
|
73
|
+
role: "tool",
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
});
|
|
77
|
+
emit({ type: EventType.RUN_FINISHED, threadId, runId });
|
|
78
|
+
}
|
|
79
|
+
catch (error) {
|
|
80
|
+
emit({
|
|
81
|
+
type: EventType.RUN_ERROR,
|
|
82
|
+
message: error instanceof Error ? error.message : String(error),
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
res.end();
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
//# sourceMappingURL=agui.js.map
|
package/dist/agui.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agui.js","sourceRoot":"","sources":["../src/agui.ts"],"names":[],"mappings":"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"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { runLinkedInAgent, isProvider, PROVIDERS } from "./providers.js";
|
|
4
|
+
function parseArgs(argv) {
|
|
5
|
+
let provider;
|
|
6
|
+
const rest = [];
|
|
7
|
+
for (let i = 0; i < argv.length; i++) {
|
|
8
|
+
const arg = argv[i];
|
|
9
|
+
if (arg === "--provider" || arg === "-p") {
|
|
10
|
+
const value = argv[++i];
|
|
11
|
+
if (!isProvider(value)) {
|
|
12
|
+
throw new Error(`--provider must be one of ${PROVIDERS.join(", ")}, got "${value ?? "<missing>"}"`);
|
|
13
|
+
}
|
|
14
|
+
provider = value;
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
rest.push(arg);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return { provider, prompt: rest.join(" ").trim() };
|
|
21
|
+
}
|
|
22
|
+
function resolveProvider(fromFlag) {
|
|
23
|
+
if (fromFlag)
|
|
24
|
+
return fromFlag;
|
|
25
|
+
const fromEnv = process.env.PROVIDER;
|
|
26
|
+
if (isProvider(fromEnv))
|
|
27
|
+
return fromEnv;
|
|
28
|
+
if (fromEnv) {
|
|
29
|
+
throw new Error(`PROVIDER must be one of ${PROVIDERS.join(", ")}, got "${fromEnv}"`);
|
|
30
|
+
}
|
|
31
|
+
return "openai";
|
|
32
|
+
}
|
|
33
|
+
async function main() {
|
|
34
|
+
const { provider: providerFlag, prompt } = parseArgs(process.argv.slice(2));
|
|
35
|
+
if (!prompt) {
|
|
36
|
+
console.error(`Usage: linkedin-agent [--provider ${PROVIDERS.join("|")}] "<prompt>"`);
|
|
37
|
+
console.error("(provider also settable via the PROVIDER env var; defaults to openai)");
|
|
38
|
+
process.exitCode = 1;
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const provider = resolveProvider(providerFlag);
|
|
42
|
+
const linkedinMcpUrl = process.env.LINKEDIN_MCP_URL ?? "http://localhost:4300";
|
|
43
|
+
const answer = await runLinkedInAgent(provider, {
|
|
44
|
+
prompt,
|
|
45
|
+
linkedinMcpUrl,
|
|
46
|
+
onToolCall: (name, args) => {
|
|
47
|
+
console.error(`[${provider}] tool call: ${name}(${JSON.stringify(args)})`);
|
|
48
|
+
},
|
|
49
|
+
});
|
|
50
|
+
console.log(answer);
|
|
51
|
+
}
|
|
52
|
+
main().catch((error) => {
|
|
53
|
+
console.error(error);
|
|
54
|
+
process.exitCode = 1;
|
|
55
|
+
});
|
|
56
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,gBAAgB,EAAE,UAAU,EAAE,SAAS,EAAiB,MAAM,gBAAgB,CAAC;AAExF,SAAS,SAAS,CAAC,IAAc;IAC/B,IAAI,QAA8B,CAAC;IACnC,MAAM,IAAI,GAAa,EAAE,CAAC;IAE1B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QACpB,IAAI,GAAG,KAAK,YAAY,IAAI,GAAG,KAAK,IAAI,EAAE,CAAC;YACzC,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC;YACxB,IAAI,CAAC,UAAU,CAAC,KAAK,CAAC,EAAE,CAAC;gBACvB,MAAM,IAAI,KAAK,CAAC,6BAA6B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,KAAK,IAAI,WAAW,GAAG,CAAC,CAAC;YACtG,CAAC;YACD,QAAQ,GAAG,KAAK,CAAC;QACnB,CAAC;aAAM,CAAC;YACN,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QACjB,CAAC;IACH,CAAC;IAED,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC;AACrD,CAAC;AAED,SAAS,eAAe,CAAC,QAA8B;IACrD,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;IACrC,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACxC,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,2BAA2B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,OAAO,GAAG,CAAC,CAAC;IACvF,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,KAAK,UAAU,IAAI;IACjB,MAAM,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,EAAE,GAAG,SAAS,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAE5E,IAAI,CAAC,MAAM,EAAE,CAAC;QACZ,OAAO,CAAC,KAAK,CAAC,qCAAqC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QACtF,OAAO,CAAC,KAAK,CAAC,uEAAuE,CAAC,CAAC;QACvF,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,QAAQ,GAAG,eAAe,CAAC,YAAY,CAAC,CAAC;IAC/C,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,uBAAuB,CAAC;IAE/E,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,QAAQ,EAAE;QAC9C,MAAM;QACN,cAAc;QACd,UAAU,EAAE,CAAC,IAAI,EAAE,IAAI,EAAE,EAAE;YACzB,OAAO,CAAC,KAAK,CAAC,IAAI,QAAQ,gBAAgB,IAAI,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC7E,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC;AACtB,CAAC;AAED,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,KAAK,EAAE,EAAE;IACrB,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IACrB,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;AACvB,CAAC,CAAC,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,oBAAoB,EAAE,oBAAoB,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAErH,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAE3C,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC"}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mirrors agents/github-agent's and agents/whatsapp-agent's providers.ts.
|
|
3
|
+
* Adding a new remote MCP bridge means: add its `runWith*` function below,
|
|
4
|
+
* add it here, and add its API key/model env vars to the README/.env.example.
|
|
5
|
+
*/
|
|
6
|
+
export const PROVIDERS = ["openai", "claude", "deepseek"];
|
|
7
|
+
export function isProvider(value) {
|
|
8
|
+
return typeof value === "string" && PROVIDERS.includes(value);
|
|
9
|
+
}
|
|
10
|
+
export const LINKEDIN_SERVER_NAME = "linkedin";
|
|
11
|
+
/** `baseUrl` is linkedin-mcp-server's origin (e.g. "http://localhost:4300") — its MCP endpoint is at /mcp. */
|
|
12
|
+
export function linkedinServerConfig(baseUrl) {
|
|
13
|
+
return { name: LINKEDIN_SERVER_NAME, url: new URL("/mcp", baseUrl).toString(), transport: "http" };
|
|
14
|
+
}
|
|
15
|
+
export const DEFAULT_SYSTEM_PROMPT = [
|
|
16
|
+
"You are an assistant for the user's personal LinkedIn account. You have",
|
|
17
|
+
"exactly two tools: get_profile (read their own profile) and create_post",
|
|
18
|
+
"(publish a text post to their own feed). You cannot send messages or",
|
|
19
|
+
"read/reply to anyone else's posts or messages — those aren't available",
|
|
20
|
+
"via LinkedIn's public API. If asked to do something outside those two",
|
|
21
|
+
"tools, say so plainly instead of pretending to do it.",
|
|
22
|
+
].join(" ");
|
|
23
|
+
async function runWithOpenAi(options) {
|
|
24
|
+
const { McpManager, runAgent } = await import("@clickonsearch/openai-remote-mcp-bridge");
|
|
25
|
+
const { default: OpenAI } = await import("openai");
|
|
26
|
+
const apiKey = process.env.OPENAI_API_KEY;
|
|
27
|
+
if (!apiKey) {
|
|
28
|
+
throw new Error("Missing OPENAI_API_KEY environment variable");
|
|
29
|
+
}
|
|
30
|
+
const mcp = new McpManager();
|
|
31
|
+
await mcp.connectAll([linkedinServerConfig(options.linkedinMcpUrl)]);
|
|
32
|
+
try {
|
|
33
|
+
const openai = new OpenAI({ apiKey });
|
|
34
|
+
return await runAgent({
|
|
35
|
+
openai,
|
|
36
|
+
model: process.env.OPENAI_MODEL ?? "gpt-4.1",
|
|
37
|
+
systemPrompt: options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
|
|
38
|
+
userPrompt: options.prompt,
|
|
39
|
+
mcp,
|
|
40
|
+
onToolCall: options.onToolCall,
|
|
41
|
+
onToolResult: options.onToolResult,
|
|
42
|
+
onAssistantText: options.onAssistantText,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
finally {
|
|
46
|
+
await mcp.closeAll();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function runWithClaude(options) {
|
|
50
|
+
const { McpManager, runAgent } = await import("@clickonsearch/claude-remote-mcp-bridge");
|
|
51
|
+
const { default: Anthropic } = await import("@anthropic-ai/sdk");
|
|
52
|
+
const apiKey = process.env.ANTHROPIC_API_KEY;
|
|
53
|
+
if (!apiKey) {
|
|
54
|
+
throw new Error("Missing ANTHROPIC_API_KEY environment variable");
|
|
55
|
+
}
|
|
56
|
+
const mcp = new McpManager();
|
|
57
|
+
await mcp.connectAll([linkedinServerConfig(options.linkedinMcpUrl)]);
|
|
58
|
+
try {
|
|
59
|
+
const anthropic = new Anthropic({ apiKey });
|
|
60
|
+
return await runAgent({
|
|
61
|
+
anthropic,
|
|
62
|
+
model: process.env.ANTHROPIC_MODEL ?? "claude-sonnet-5",
|
|
63
|
+
systemPrompt: options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
|
|
64
|
+
userPrompt: options.prompt,
|
|
65
|
+
mcp,
|
|
66
|
+
onToolCall: options.onToolCall,
|
|
67
|
+
onToolResult: options.onToolResult,
|
|
68
|
+
onAssistantText: options.onAssistantText,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
finally {
|
|
72
|
+
await mcp.closeAll();
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
async function runWithDeepSeek(options) {
|
|
76
|
+
const { McpManager, runAgent } = await import("@clickonsearch/deepseek-remote-mcp-bridge");
|
|
77
|
+
const { default: OpenAI } = await import("openai");
|
|
78
|
+
const apiKey = process.env.DEEPSEEK_API_KEY;
|
|
79
|
+
if (!apiKey) {
|
|
80
|
+
throw new Error("Missing DEEPSEEK_API_KEY environment variable");
|
|
81
|
+
}
|
|
82
|
+
const mcp = new McpManager();
|
|
83
|
+
await mcp.connectAll([linkedinServerConfig(options.linkedinMcpUrl)]);
|
|
84
|
+
try {
|
|
85
|
+
const deepseek = new OpenAI({
|
|
86
|
+
apiKey,
|
|
87
|
+
baseURL: process.env.DEEPSEEK_BASE_URL ?? "https://api.deepseek.com",
|
|
88
|
+
});
|
|
89
|
+
return await runAgent({
|
|
90
|
+
deepseek,
|
|
91
|
+
model: process.env.DEEPSEEK_MODEL ?? "deepseek-chat",
|
|
92
|
+
systemPrompt: options.systemPrompt ?? DEFAULT_SYSTEM_PROMPT,
|
|
93
|
+
userPrompt: options.prompt,
|
|
94
|
+
mcp,
|
|
95
|
+
onToolCall: options.onToolCall,
|
|
96
|
+
onToolResult: options.onToolResult,
|
|
97
|
+
onAssistantText: options.onAssistantText,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
finally {
|
|
101
|
+
await mcp.closeAll();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Runs the LinkedIn agent through the requested provider's remote MCP bridge. */
|
|
105
|
+
export async function runLinkedInAgent(provider, options) {
|
|
106
|
+
if (provider === "claude")
|
|
107
|
+
return runWithClaude(options);
|
|
108
|
+
if (provider === "deepseek")
|
|
109
|
+
return runWithDeepSeek(options);
|
|
110
|
+
return runWithOpenAi(options);
|
|
111
|
+
}
|
|
112
|
+
//# sourceMappingURL=providers.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"providers.js","sourceRoot":"","sources":["../src/providers.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import "dotenv/config";
|
|
3
|
+
import { createServer } from "./server.js";
|
|
4
|
+
import { isProvider, PROVIDERS } from "./providers.js";
|
|
5
|
+
function resolveDefaultProvider() {
|
|
6
|
+
const fromEnv = process.env.PROVIDER;
|
|
7
|
+
if (isProvider(fromEnv))
|
|
8
|
+
return fromEnv;
|
|
9
|
+
if (fromEnv) {
|
|
10
|
+
throw new Error(`PROVIDER must be one of ${PROVIDERS.join(", ")}, got "${fromEnv}"`);
|
|
11
|
+
}
|
|
12
|
+
return "openai";
|
|
13
|
+
}
|
|
14
|
+
function main() {
|
|
15
|
+
let defaultProvider;
|
|
16
|
+
try {
|
|
17
|
+
defaultProvider = resolveDefaultProvider();
|
|
18
|
+
}
|
|
19
|
+
catch (error) {
|
|
20
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
21
|
+
process.exitCode = 1;
|
|
22
|
+
return;
|
|
23
|
+
}
|
|
24
|
+
const linkedinMcpUrl = process.env.LINKEDIN_MCP_URL ?? "http://localhost:4300";
|
|
25
|
+
const port = Number(process.env.PORT ?? 3200);
|
|
26
|
+
const app = createServer({ linkedinMcpUrl, defaultProvider });
|
|
27
|
+
app.listen(port, () => {
|
|
28
|
+
console.error(`linkedin-agent AG-UI server listening on http://localhost:${port}`);
|
|
29
|
+
console.error(` AG-UI endpoint: POST http://localhost:${port}/agent`);
|
|
30
|
+
console.error(` Chat UI: http://localhost:${port}/`);
|
|
31
|
+
console.error(` LinkedIn MCP server: ${linkedinMcpUrl}`);
|
|
32
|
+
console.error(` Default provider: ${defaultProvider} (override per-request via forwardedProps.provider)`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
main();
|
|
36
|
+
//# sourceMappingURL=server-cli.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-cli.js","sourceRoot":"","sources":["../src/server-cli.ts"],"names":[],"mappings":";AACA,OAAO,eAAe,CAAC;AACvB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAC3C,OAAO,EAAE,UAAU,EAAE,SAAS,EAAiB,MAAM,gBAAgB,CAAC;AAEtE,SAAS,sBAAsB;IAC7B,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,QAAQ,CAAC;IACrC,IAAI,UAAU,CAAC,OAAO,CAAC;QAAE,OAAO,OAAO,CAAC;IACxC,IAAI,OAAO,EAAE,CAAC;QACZ,MAAM,IAAI,KAAK,CAAC,2BAA2B,SAAS,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,OAAO,GAAG,CAAC,CAAC;IACvF,CAAC;IACD,OAAO,QAAQ,CAAC;AAClB,CAAC;AAED,SAAS,IAAI;IACX,IAAI,eAAyB,CAAC;IAC9B,IAAI,CAAC;QACH,eAAe,GAAG,sBAAsB,EAAE,CAAC;IAC7C,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,OAAO,CAAC,KAAK,CAAC,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;QACtE,OAAO,CAAC,QAAQ,GAAG,CAAC,CAAC;QACrB,OAAO;IACT,CAAC;IAED,MAAM,cAAc,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,IAAI,uBAAuB,CAAC;IAC/E,MAAM,IAAI,GAAG,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC,CAAC;IAE9C,MAAM,GAAG,GAAG,YAAY,CAAC,EAAE,cAAc,EAAE,eAAe,EAAE,CAAC,CAAC;IAE9D,GAAG,CAAC,MAAM,CAAC,IAAI,EAAE,GAAG,EAAE;QACpB,OAAO,CAAC,KAAK,CAAC,6DAA6D,IAAI,EAAE,CAAC,CAAC;QACnF,OAAO,CAAC,KAAK,CAAC,2CAA2C,IAAI,QAAQ,CAAC,CAAC;QACvE,OAAO,CAAC,KAAK,CAAC,sCAAsC,IAAI,GAAG,CAAC,CAAC;QAC7D,OAAO,CAAC,KAAK,CAAC,0BAA0B,cAAc,EAAE,CAAC,CAAC;QAC1D,OAAO,CAAC,KAAK,CAAC,uBAAuB,eAAe,qDAAqD,CAAC,CAAC;IAC7G,CAAC,CAAC,CAAC;AACL,CAAC;AAED,IAAI,EAAE,CAAC"}
|
package/dist/server.js
ADDED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import path from "node:path";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import express from "express";
|
|
4
|
+
import cors from "cors";
|
|
5
|
+
import { runAgUiRequest } from "./agui.js";
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
export function createServer(options) {
|
|
8
|
+
const app = express();
|
|
9
|
+
app.use(cors());
|
|
10
|
+
app.use(express.json({ limit: "2mb" }));
|
|
11
|
+
app.use(express.static(path.join(__dirname, "..", "public")));
|
|
12
|
+
app.get("/health", (_req, res) => {
|
|
13
|
+
res.json({ ok: true, defaultProvider: options.defaultProvider });
|
|
14
|
+
});
|
|
15
|
+
app.post("/agent", async (req, res) => {
|
|
16
|
+
const body = req.body;
|
|
17
|
+
if (!body || !Array.isArray(body.messages)) {
|
|
18
|
+
res.status(400).json({ error: 'Request body must be a RunAgentInput with a "messages" array' });
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const input = {
|
|
22
|
+
threadId: body.threadId ?? "",
|
|
23
|
+
runId: body.runId ?? "",
|
|
24
|
+
messages: body.messages,
|
|
25
|
+
tools: body.tools ?? [],
|
|
26
|
+
context: body.context ?? [],
|
|
27
|
+
forwardedProps: body.forwardedProps,
|
|
28
|
+
};
|
|
29
|
+
try {
|
|
30
|
+
await runAgUiRequest({
|
|
31
|
+
input,
|
|
32
|
+
linkedinMcpUrl: options.linkedinMcpUrl,
|
|
33
|
+
defaultProvider: options.defaultProvider,
|
|
34
|
+
res,
|
|
35
|
+
acceptHeader: req.headers.accept,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
catch (error) {
|
|
39
|
+
if (!res.headersSent) {
|
|
40
|
+
res.status(500).json({ error: error instanceof Error ? error.message : String(error) });
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
res.end();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
return app;
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=server.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AACzC,OAAO,OAAO,MAAM,SAAS,CAAC;AAC9B,OAAO,IAAI,MAAM,MAAM,CAAC;AAExB,OAAO,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAG3C,MAAM,SAAS,GAAG,IAAI,CAAC,OAAO,CAAC,aAAa,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC;AAO/D,MAAM,UAAU,YAAY,CAAC,OAA4B;IACvD,MAAM,GAAG,GAAG,OAAO,EAAE,CAAC;IACtB,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC;IAChB,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC;IACxC,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,EAAE,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC,CAAC;IAE9D,GAAG,CAAC,GAAG,CAAC,SAAS,EAAE,CAAC,IAAI,EAAE,GAAG,EAAE,EAAE;QAC/B,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,eAAe,EAAE,OAAO,CAAC,eAAe,EAAE,CAAC,CAAC;IACnE,CAAC,CAAC,CAAC;IAEH,GAAG,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,EAAE,GAAG,EAAE,GAAG,EAAE,EAAE;QACpC,MAAM,IAAI,GAAG,GAAG,CAAC,IAA0C,CAAC;QAE5D,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC;YAC3C,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,8DAA8D,EAAE,CAAC,CAAC;YAChG,OAAO;QACT,CAAC;QAED,MAAM,KAAK,GAAkB;YAC3B,QAAQ,EAAE,IAAI,CAAC,QAAQ,IAAI,EAAE;YAC7B,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;YACvB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,KAAK,EAAE,IAAI,CAAC,KAAK,IAAI,EAAE;YACvB,OAAO,EAAE,IAAI,CAAC,OAAO,IAAI,EAAE;YAC3B,cAAc,EAAE,IAAI,CAAC,cAAc;SACpC,CAAC;QAEF,IAAI,CAAC;YACH,MAAM,cAAc,CAAC;gBACnB,KAAK;gBACL,cAAc,EAAE,OAAO,CAAC,cAAc;gBACtC,eAAe,EAAE,OAAO,CAAC,eAAe;gBACxC,GAAG;gBACH,YAAY,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM;aACjC,CAAC,CAAC;QACL,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,CAAC,GAAG,CAAC,WAAW,EAAE,CAAC;gBACrB,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;YAC1F,CAAC;iBAAM,CAAC;gBACN,GAAG,CAAC,GAAG,EAAE,CAAC;YACZ,CAAC;QACH,CAAC;IACH,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@clickonsearch/linkedin-agent",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Agent for a personal LinkedIn account (read your profile, publish posts) via linkedin-mcp-server, routed through OpenAI, Claude, or DeepSeek depending on preference. Ships an AG-UI protocol server plus a browser chat UI.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/ClickOnSearch/ai-apps.git",
|
|
10
|
+
"directory": "agents/linkedin-agent"
|
|
11
|
+
},
|
|
12
|
+
"publishConfig": {
|
|
13
|
+
"access": "public"
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist",
|
|
17
|
+
"public"
|
|
18
|
+
],
|
|
19
|
+
"main": "dist/index.js",
|
|
20
|
+
"bin": {
|
|
21
|
+
"linkedin-agent": "dist/cli.js",
|
|
22
|
+
"linkedin-agent-serve": "dist/server-cli.js"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc -p tsconfig.json",
|
|
26
|
+
"start": "node dist/cli.js",
|
|
27
|
+
"dev": "tsx src/cli.ts",
|
|
28
|
+
"serve": "node dist/server-cli.js",
|
|
29
|
+
"dev:serve": "tsx src/server-cli.ts"
|
|
30
|
+
},
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"@clickonsearch/openai-remote-mcp-bridge": "^0.1.0",
|
|
33
|
+
"@clickonsearch/claude-remote-mcp-bridge": "^0.1.0",
|
|
34
|
+
"@clickonsearch/deepseek-remote-mcp-bridge": "^0.1.0",
|
|
35
|
+
"openai": "^4.68.0",
|
|
36
|
+
"@anthropic-ai/sdk": "^0.32.1",
|
|
37
|
+
"@ag-ui/core": "^1.0.0",
|
|
38
|
+
"@ag-ui/encoder": "^1.0.0",
|
|
39
|
+
"express": "^4.21.1",
|
|
40
|
+
"cors": "^2.8.5",
|
|
41
|
+
"dotenv": "^16.4.5"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"typescript": "^5.6.3",
|
|
45
|
+
"tsx": "^4.19.1",
|
|
46
|
+
"@types/node": "^22.7.5",
|
|
47
|
+
"@types/express": "^4.17.21",
|
|
48
|
+
"@types/cors": "^2.8.17"
|
|
49
|
+
},
|
|
50
|
+
"engines": {
|
|
51
|
+
"node": ">=18"
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
<!doctype html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
6
|
+
<title>LinkedIn Assistant</title>
|
|
7
|
+
<style>
|
|
8
|
+
:root {
|
|
9
|
+
color-scheme: light dark;
|
|
10
|
+
--bg: #0b0d12;
|
|
11
|
+
--panel: #12151c;
|
|
12
|
+
--border: #232833;
|
|
13
|
+
--text: #e6e9ef;
|
|
14
|
+
--muted: #8b93a3;
|
|
15
|
+
--accent: #0a66c2;
|
|
16
|
+
--user-bubble: #1f6feb22;
|
|
17
|
+
--assistant-bubble: #171b24;
|
|
18
|
+
--tool-bubble: #1a2a1f;
|
|
19
|
+
--error-bubble: #3a1414;
|
|
20
|
+
}
|
|
21
|
+
* { box-sizing: border-box; }
|
|
22
|
+
body {
|
|
23
|
+
margin: 0;
|
|
24
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
25
|
+
background: var(--bg);
|
|
26
|
+
color: var(--text);
|
|
27
|
+
height: 100vh;
|
|
28
|
+
display: flex;
|
|
29
|
+
flex-direction: column;
|
|
30
|
+
}
|
|
31
|
+
header {
|
|
32
|
+
display: flex;
|
|
33
|
+
align-items: center;
|
|
34
|
+
justify-content: space-between;
|
|
35
|
+
padding: 12px 20px;
|
|
36
|
+
border-bottom: 1px solid var(--border);
|
|
37
|
+
background: var(--panel);
|
|
38
|
+
}
|
|
39
|
+
header h1 {
|
|
40
|
+
font-size: 15px;
|
|
41
|
+
font-weight: 600;
|
|
42
|
+
margin: 0;
|
|
43
|
+
}
|
|
44
|
+
header .sub {
|
|
45
|
+
color: var(--muted);
|
|
46
|
+
font-size: 12px;
|
|
47
|
+
margin-top: 2px;
|
|
48
|
+
}
|
|
49
|
+
select {
|
|
50
|
+
background: var(--panel);
|
|
51
|
+
color: var(--text);
|
|
52
|
+
border: 1px solid var(--border);
|
|
53
|
+
border-radius: 6px;
|
|
54
|
+
padding: 6px 10px;
|
|
55
|
+
font-size: 13px;
|
|
56
|
+
}
|
|
57
|
+
#log {
|
|
58
|
+
flex: 1;
|
|
59
|
+
overflow-y: auto;
|
|
60
|
+
padding: 20px;
|
|
61
|
+
display: flex;
|
|
62
|
+
flex-direction: column;
|
|
63
|
+
gap: 12px;
|
|
64
|
+
max-width: 820px;
|
|
65
|
+
width: 100%;
|
|
66
|
+
margin: 0 auto;
|
|
67
|
+
}
|
|
68
|
+
.bubble {
|
|
69
|
+
padding: 10px 14px;
|
|
70
|
+
border-radius: 10px;
|
|
71
|
+
max-width: 100%;
|
|
72
|
+
white-space: pre-wrap;
|
|
73
|
+
word-break: break-word;
|
|
74
|
+
line-height: 1.45;
|
|
75
|
+
font-size: 14px;
|
|
76
|
+
}
|
|
77
|
+
.user { align-self: flex-end; background: var(--user-bubble); border: 1px solid #1f6feb55; }
|
|
78
|
+
.assistant { align-self: flex-start; background: var(--assistant-bubble); border: 1px solid var(--border); }
|
|
79
|
+
.tool { align-self: flex-start; background: var(--tool-bubble); border: 1px solid #2f6b3f55; font-size: 12.5px; color: var(--muted); font-family: ui-monospace, monospace; }
|
|
80
|
+
.tool .name { color: #8fd19e; }
|
|
81
|
+
.tool .result { display: block; margin-top: 6px; color: var(--text); white-space: pre-wrap; max-height: 220px; overflow-y: auto; }
|
|
82
|
+
.error { align-self: center; background: var(--error-bubble); border: 1px solid #7a2424; color: #ffb4b4; font-size: 13px; }
|
|
83
|
+
.status { align-self: flex-start; color: var(--muted); font-size: 12.5px; font-style: italic; }
|
|
84
|
+
footer {
|
|
85
|
+
border-top: 1px solid var(--border);
|
|
86
|
+
background: var(--panel);
|
|
87
|
+
padding: 14px 20px;
|
|
88
|
+
}
|
|
89
|
+
form {
|
|
90
|
+
max-width: 820px;
|
|
91
|
+
margin: 0 auto;
|
|
92
|
+
display: flex;
|
|
93
|
+
gap: 10px;
|
|
94
|
+
}
|
|
95
|
+
textarea {
|
|
96
|
+
flex: 1;
|
|
97
|
+
resize: none;
|
|
98
|
+
background: #0e1117;
|
|
99
|
+
color: var(--text);
|
|
100
|
+
border: 1px solid var(--border);
|
|
101
|
+
border-radius: 8px;
|
|
102
|
+
padding: 10px 12px;
|
|
103
|
+
font-size: 14px;
|
|
104
|
+
font-family: inherit;
|
|
105
|
+
min-height: 42px;
|
|
106
|
+
max-height: 160px;
|
|
107
|
+
}
|
|
108
|
+
button {
|
|
109
|
+
background: var(--accent);
|
|
110
|
+
color: white;
|
|
111
|
+
border: none;
|
|
112
|
+
border-radius: 8px;
|
|
113
|
+
padding: 0 18px;
|
|
114
|
+
font-size: 14px;
|
|
115
|
+
font-weight: 600;
|
|
116
|
+
cursor: pointer;
|
|
117
|
+
}
|
|
118
|
+
button:disabled { opacity: 0.5; cursor: default; }
|
|
119
|
+
</style>
|
|
120
|
+
</head>
|
|
121
|
+
<body>
|
|
122
|
+
<header>
|
|
123
|
+
<div>
|
|
124
|
+
<h1>LinkedIn assistant</h1>
|
|
125
|
+
<div class="sub">reads your profile and posts to your feed via linkedin-mcp-server, over an AG-UI protocol stream</div>
|
|
126
|
+
</div>
|
|
127
|
+
<select id="provider">
|
|
128
|
+
<option value="openai">OpenAI</option>
|
|
129
|
+
<option value="claude">Claude</option>
|
|
130
|
+
<option value="deepseek">DeepSeek</option>
|
|
131
|
+
</select>
|
|
132
|
+
</header>
|
|
133
|
+
|
|
134
|
+
<div id="log"></div>
|
|
135
|
+
|
|
136
|
+
<footer>
|
|
137
|
+
<form id="form">
|
|
138
|
+
<textarea id="input" placeholder="Ask about your profile, or ask it to post something..." rows="1"></textarea>
|
|
139
|
+
<button id="send" type="submit">Send</button>
|
|
140
|
+
</form>
|
|
141
|
+
</footer>
|
|
142
|
+
|
|
143
|
+
<script>
|
|
144
|
+
(() => {
|
|
145
|
+
const log = document.getElementById("log");
|
|
146
|
+
const form = document.getElementById("form");
|
|
147
|
+
const input = document.getElementById("input");
|
|
148
|
+
const sendButton = document.getElementById("send");
|
|
149
|
+
const providerSelect = document.getElementById("provider");
|
|
150
|
+
|
|
151
|
+
const threadId = crypto.randomUUID();
|
|
152
|
+
/** @type {{id: string, role: string, content: string}[]} */
|
|
153
|
+
const messages = [];
|
|
154
|
+
|
|
155
|
+
// Reflect the server's actual configured default (PROVIDER env var)
|
|
156
|
+
// instead of always showing the dropdown's first option.
|
|
157
|
+
fetch("/health")
|
|
158
|
+
.then((res) => res.json())
|
|
159
|
+
.then((data) => {
|
|
160
|
+
if (data.defaultProvider && providerSelect.querySelector(`option[value="${data.defaultProvider}"]`)) {
|
|
161
|
+
providerSelect.value = data.defaultProvider;
|
|
162
|
+
}
|
|
163
|
+
})
|
|
164
|
+
.catch(() => {});
|
|
165
|
+
|
|
166
|
+
function bubble(className, text) {
|
|
167
|
+
const el = document.createElement("div");
|
|
168
|
+
el.className = "bubble " + className;
|
|
169
|
+
el.textContent = text;
|
|
170
|
+
log.appendChild(el);
|
|
171
|
+
log.scrollTop = log.scrollHeight;
|
|
172
|
+
return el;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function setBusy(busy) {
|
|
176
|
+
sendButton.disabled = busy;
|
|
177
|
+
input.disabled = busy;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async function send(text) {
|
|
181
|
+
messages.push({ id: crypto.randomUUID(), role: "user", content: text });
|
|
182
|
+
bubble("user", text);
|
|
183
|
+
|
|
184
|
+
setBusy(true);
|
|
185
|
+
const statusEl = bubble("status", "thinking…");
|
|
186
|
+
|
|
187
|
+
const textBubbles = new Map(); // messageId -> { el, buffer }
|
|
188
|
+
const toolBubbles = new Map(); // toolCallId -> { el, nameEl, resultEl }
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const res = await fetch("/agent", {
|
|
192
|
+
method: "POST",
|
|
193
|
+
headers: { "Content-Type": "application/json" },
|
|
194
|
+
body: JSON.stringify({
|
|
195
|
+
threadId,
|
|
196
|
+
runId: crypto.randomUUID(),
|
|
197
|
+
messages,
|
|
198
|
+
forwardedProps: { provider: providerSelect.value },
|
|
199
|
+
}),
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
if (!res.ok || !res.body) {
|
|
203
|
+
throw new Error(`Request failed: ${res.status} ${res.statusText}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const reader = res.body.getReader();
|
|
207
|
+
const decoder = new TextDecoder();
|
|
208
|
+
let buffer = "";
|
|
209
|
+
|
|
210
|
+
while (true) {
|
|
211
|
+
const { done, value } = await reader.read();
|
|
212
|
+
if (done) break;
|
|
213
|
+
buffer += decoder.decode(value, { stream: true });
|
|
214
|
+
|
|
215
|
+
let sep;
|
|
216
|
+
while ((sep = buffer.indexOf("\n\n")) !== -1) {
|
|
217
|
+
const frame = buffer.slice(0, sep);
|
|
218
|
+
buffer = buffer.slice(sep + 2);
|
|
219
|
+
const line = frame.split("\n").find((l) => l.startsWith("data:"));
|
|
220
|
+
if (!line) continue;
|
|
221
|
+
|
|
222
|
+
const event = JSON.parse(line.slice(5).trim());
|
|
223
|
+
handleEvent(event);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
} catch (err) {
|
|
227
|
+
bubble("error", String(err));
|
|
228
|
+
} finally {
|
|
229
|
+
statusEl.remove();
|
|
230
|
+
setBusy(false);
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function handleEvent(event) {
|
|
234
|
+
switch (event.type) {
|
|
235
|
+
case "TEXT_MESSAGE_START": {
|
|
236
|
+
const el = bubble("assistant", "");
|
|
237
|
+
textBubbles.set(event.messageId, { el, buffer: "" });
|
|
238
|
+
break;
|
|
239
|
+
}
|
|
240
|
+
case "TEXT_MESSAGE_CONTENT": {
|
|
241
|
+
const entry = textBubbles.get(event.messageId);
|
|
242
|
+
if (entry) {
|
|
243
|
+
entry.buffer += event.delta;
|
|
244
|
+
entry.el.textContent = entry.buffer;
|
|
245
|
+
log.scrollTop = log.scrollHeight;
|
|
246
|
+
}
|
|
247
|
+
break;
|
|
248
|
+
}
|
|
249
|
+
case "TEXT_MESSAGE_END": {
|
|
250
|
+
const entry = textBubbles.get(event.messageId);
|
|
251
|
+
if (entry) {
|
|
252
|
+
messages.push({ id: event.messageId, role: "assistant", content: entry.buffer });
|
|
253
|
+
}
|
|
254
|
+
break;
|
|
255
|
+
}
|
|
256
|
+
case "TOOL_CALL_START": {
|
|
257
|
+
const el = document.createElement("div");
|
|
258
|
+
el.className = "bubble tool";
|
|
259
|
+
const nameEl = document.createElement("span");
|
|
260
|
+
nameEl.className = "name";
|
|
261
|
+
nameEl.textContent = "🔧 " + event.toolCallName;
|
|
262
|
+
el.appendChild(nameEl);
|
|
263
|
+
const resultEl = document.createElement("span");
|
|
264
|
+
resultEl.className = "result";
|
|
265
|
+
resultEl.textContent = "running…";
|
|
266
|
+
el.appendChild(resultEl);
|
|
267
|
+
log.appendChild(el);
|
|
268
|
+
log.scrollTop = log.scrollHeight;
|
|
269
|
+
toolBubbles.set(event.toolCallId, { el, nameEl, resultEl });
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
case "TOOL_CALL_RESULT": {
|
|
273
|
+
const entry = toolBubbles.get(event.toolCallId);
|
|
274
|
+
if (entry) {
|
|
275
|
+
const text = typeof event.content === "string" ? event.content : JSON.stringify(event.content);
|
|
276
|
+
entry.resultEl.textContent = text.length > 2000 ? text.slice(0, 2000) + "…" : text;
|
|
277
|
+
log.scrollTop = log.scrollHeight;
|
|
278
|
+
}
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
case "RUN_ERROR": {
|
|
282
|
+
bubble("error", event.message);
|
|
283
|
+
break;
|
|
284
|
+
}
|
|
285
|
+
default:
|
|
286
|
+
break;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
form.addEventListener("submit", (e) => {
|
|
292
|
+
e.preventDefault();
|
|
293
|
+
const text = input.value.trim();
|
|
294
|
+
if (!text) return;
|
|
295
|
+
input.value = "";
|
|
296
|
+
input.style.height = "auto";
|
|
297
|
+
send(text);
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
input.addEventListener("keydown", (e) => {
|
|
301
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
302
|
+
e.preventDefault();
|
|
303
|
+
form.requestSubmit();
|
|
304
|
+
}
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
input.addEventListener("input", () => {
|
|
308
|
+
input.style.height = "auto";
|
|
309
|
+
input.style.height = Math.min(input.scrollHeight, 160) + "px";
|
|
310
|
+
});
|
|
311
|
+
})();
|
|
312
|
+
</script>
|
|
313
|
+
</body>
|
|
314
|
+
</html>
|