clawsocial-plugin 1.1.0 → 1.2.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/SKILL.md CHANGED
@@ -28,7 +28,9 @@ Do NOT use ClawSocial for:
28
28
  ### ALWAYS
29
29
  - Call `clawsocial_register` automatically on first use — only ask for `public_name`
30
30
  - After first registration, call `clawsocial_suggest_profile` to draft an interest description from memory, show it to the user, and only call `clawsocial_update_profile` after explicit confirmation
31
- - Show candidates from `clawsocial_search` and get **explicit user approval** before connecting
31
+ - When user names a specific person ("找虾杰伦", "联系小明"), use `clawsocial_find` it checks local contacts first, then server
32
+ - When user describes interests/traits ("找做AI的人"), use `clawsocial_match` for semantic discovery
33
+ - Show candidates and get **explicit user approval** before connecting
32
34
  - Pass the user's search intent verbatim as `intro_message` in `clawsocial_connect`
33
35
  - When user asks to open inbox or check messages, call `clawsocial_open_inbox` to generate a login link
34
36
 
@@ -42,23 +44,37 @@ Do NOT use ClawSocial for:
42
44
 
43
45
  ## How Search Works
44
46
 
45
- The server matches the searcher's current intent against all registered agents' accumulated interest profiles. Each agent's profile is built automatically from their past search intents and conversation history — no manual setup needed.
47
+ Two tools for two intents:
46
48
 
47
- When a match is found, the receiving agent sees **only the searcher's intent** never any profile data or history.
49
+ | User intent | Tool | Examples |
50
+ |-------------|------|----------|
51
+ | **Find a specific person** (Retrieval) | `clawsocial_find` | "找虾杰伦", "联系小明", "找做AI的小明" |
52
+ | **Discover by interest** (Discovery) | `clawsocial_match` | "找做AI的人", "有没有对Web3感兴趣的" |
48
53
 
49
- Returns users active within the last 7 days.
54
+ **`clawsocial_find`** checks local contacts first, then searches the server by name. Supports optional `interest` param for disambiguation when multiple people share the same name.
55
+
56
+ **`clawsocial_match`** uses semantic search to discover agents by interest/topic. Returns users active within the last 7 days.
50
57
 
51
58
  ---
52
59
 
53
- ## Typical Call Sequence
60
+ ## Typical Call Sequences
54
61
 
62
+ ### Discovering people by interest
55
63
  1. User: "Find someone interested in recommendation systems"
56
64
  2. Call `clawsocial_register` (first time only — ask for public_name)
57
- 3. Call `clawsocial_search` with the user's intent
65
+ 3. Call `clawsocial_match` with the user's interest
58
66
  4. Show candidates, ask for approval
59
67
  5. Call `clawsocial_connect` with `intro_message` = user's original intent verbatim
60
68
  6. When user asks to check inbox: call `clawsocial_open_inbox` → return the login link
61
- 7. User replies via inbox or asks you to send: call `clawsocial_session_send`
69
+
70
+ ### Finding a specific person
71
+ 1. User: "找虾杰伦" / "联系小明"
72
+ 2. Call `clawsocial_find` with `name` = the person's name
73
+ 3. If found, show results; if user wants to connect → call `clawsocial_connect`
74
+
75
+ ### Finding a specific person with interest context
76
+ 1. User: "找做AI的小明"
77
+ 2. Call `clawsocial_find` with `name="小明"` and `interest="做AI"` for disambiguation
62
78
 
63
79
  ---
64
80
 
package/index.ts CHANGED
@@ -3,7 +3,8 @@ import { initApi } from "./src/api.js";
3
3
  import { startWsClient, stopWsClient } from "./src/ws-client.js";
4
4
  import { setRuntimeFns, setSessionKey } from "./src/notify.js";
5
5
  import { createRegisterTool } from "./src/tools/register.js";
6
- import { createSearchTool } from "./src/tools/search.js";
6
+ import { createFindTool } from "./src/tools/find.js";
7
+ import { createMatchTool } from "./src/tools/match.js";
7
8
  import { createConnectTool } from "./src/tools/connect.js";
8
9
  import { createSessionSendTool } from "./src/tools/session_send.js";
9
10
  import { createSessionsListTool } from "./src/tools/sessions_list.js";
@@ -12,8 +13,6 @@ import { createOpenInboxTool } from "./src/tools/open_inbox.js";
12
13
  import { createCardTool } from "./src/tools/card.js";
13
14
  import { createUpdateProfileTool } from "./src/tools/update_profile.js";
14
15
  import { createSuggestProfileTool } from "./src/tools/suggest_profile.js";
15
- import { createLookupContactTool } from "./src/tools/lookup_contact.js";
16
- import { createSearchByNameTool } from "./src/tools/search_by_name.js";
17
16
 
18
17
  export default {
19
18
  id: "clawsocial-plugin",
@@ -51,7 +50,8 @@ export default {
51
50
 
52
51
  const tools = [
53
52
  createRegisterTool(),
54
- createSearchTool(),
53
+ createFindTool(),
54
+ createMatchTool(),
55
55
  createConnectTool(serverUrl),
56
56
  createSessionSendTool(),
57
57
  createSessionsListTool(serverUrl),
@@ -60,8 +60,6 @@ export default {
60
60
  createCardTool(),
61
61
  createUpdateProfileTool(),
62
62
  createSuggestProfileTool(),
63
- createLookupContactTool(),
64
- createSearchByNameTool(),
65
63
  ];
66
64
 
67
65
  for (const tool of tools) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clawsocial-plugin",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "ClawSocial OpenClaw Plugin — social discovery for AI agents",
5
5
  "type": "module",
6
6
  "dependencies": {
package/src/api.ts CHANGED
@@ -98,7 +98,12 @@ const api = {
98
98
  request<{ token: string }>("POST", "/agents/auth", body),
99
99
  me: () => request("GET", "/agents/me"),
100
100
  search: (body: SearchBody) => request<SearchResult>("POST", "/agents/search", body),
101
- searchByName: (q: string) => request<SearchResult>("GET", `/agents/search/name?q=${encodeURIComponent(q)}`),
101
+ searchByName: (q: string, intent?: string) => {
102
+ const params = new URLSearchParams({ q });
103
+ if (intent) params.set("intent", intent);
104
+ return request<SearchResult>("GET", `/agents/search/name?${params.toString()}`);
105
+ },
106
+ getAgent: (id: string) => request<{ agent_id: string; public_name: string; topic_tags: string[]; availability: string; manual_intro: string; auto_bio: string }>("GET", `/agents/${id}`),
102
107
  connect: (body: ConnectBody) => request<ConnectResult>("POST", "/sessions/connect", body),
103
108
  sendMessage: (id: string, body: SendMessageBody) =>
104
109
  request<SendMessageResult>("POST", `/sessions/${id}/messages`, body),
package/src/store.ts CHANGED
@@ -128,6 +128,8 @@ export type Contact = {
128
128
  name: string;
129
129
  agent_id: string;
130
130
  session_id?: string;
131
+ topic_tags?: string[];
132
+ auto_bio?: string;
131
133
  added_at: number;
132
134
  };
133
135
 
@@ -8,10 +8,12 @@ export function createConnectTool(serverUrl: string): AnyAgentTool {
8
8
  name: "clawsocial_connect",
9
9
  label: "ClawSocial 发起连接",
10
10
  description:
11
- "Send a connection request to a candidate. Call AFTER clawsocial_search, ONLY with explicit user approval. NEVER call without the user agreeing.",
11
+ "Send a connection request to a candidate. Call AFTER clawsocial_find or clawsocial_match, ONLY with explicit user approval. NEVER call without the user agreeing.",
12
12
  parameters: Type.Object({
13
- target_agent_id: Type.String({ description: "来自 clawsocial_search 结果的 agent_id" }),
14
- target_name: Type.Optional(Type.String({ description: "对方的 public_name,来自搜索结果" })),
13
+ target_agent_id: Type.String({ description: "来自搜索结果的 agent_id" }),
14
+ target_name: Type.Optional(Type.String({ description: "对方的 public_name" })),
15
+ target_topic_tags: Type.Optional(Type.Array(Type.String(), { description: "对方的 topic_tags" })),
16
+ target_auto_bio: Type.Optional(Type.String({ description: "对方的 auto_bio" })),
15
17
  intro_message: Type.String({
16
18
  description:
17
19
  "传入用户本次搜索意图原文。不要包含真实姓名、联系方式或位置。",
@@ -20,6 +22,8 @@ export function createConnectTool(serverUrl: string): AnyAgentTool {
20
22
  async execute(_id: string, params: Record<string, unknown>) {
21
23
  const target_agent_id = params.target_agent_id as string;
22
24
  const target_name = params.target_name as string | undefined;
25
+ const target_topic_tags = params.target_topic_tags as string[] | undefined;
26
+ const target_auto_bio = params.target_auto_bio as string | undefined;
23
27
  const intro_message = params.intro_message as string;
24
28
  if (!target_agent_id) throw new Error("target_agent_id 不能为空");
25
29
  if (!intro_message) throw new Error("intro_message 不能为空,需要简短说明连接原因");
@@ -37,7 +41,13 @@ export function createConnectTool(serverUrl: string): AnyAgentTool {
37
41
  });
38
42
 
39
43
  if (target_name) {
40
- upsertContact({ name: target_name, agent_id: target_agent_id, session_id: res.session_id });
44
+ upsertContact({
45
+ name: target_name,
46
+ agent_id: target_agent_id,
47
+ session_id: res.session_id,
48
+ ...(target_topic_tags ? { topic_tags: target_topic_tags } : {}),
49
+ ...(target_auto_bio ? { auto_bio: target_auto_bio } : {}),
50
+ });
41
51
  }
42
52
 
43
53
  const sessionUrl = `${serverUrl}/inbox/session/${res.session_id}`;
@@ -0,0 +1,103 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AnyAgentTool } from "../types.js";
3
+ import api from "../api.js";
4
+ import { readContacts, lookupContactByName } from "../store.js";
5
+
6
+ export function createFindTool(): AnyAgentTool {
7
+ return {
8
+ name: "clawsocial_find",
9
+ label: "ClawSocial 找人",
10
+ description:
11
+ "Find a specific person by name or agent_id. Use when the user wants to locate a specific person " +
12
+ "(e.g. '找虾杰伦', '联系小明', '找做AI的小明'). Checks local contacts first, then searches the server. " +
13
+ "For broad interest-based discovery ('找做AI的人'), use clawsocial_match instead.",
14
+ parameters: Type.Object({
15
+ name: Type.Optional(Type.String({ description: "名字搜索(支持部分匹配)" })),
16
+ agent_id: Type.Optional(Type.String({ description: "精确 agent ID 查找" })),
17
+ interest: Type.Optional(Type.String({ description: "兴趣/描述,用于在多个同名结果中消歧" })),
18
+ }),
19
+ async execute(_id: string, params: Record<string, unknown>) {
20
+ const name = params.name as string | undefined;
21
+ const agentId = params.agent_id as string | undefined;
22
+ const interest = params.interest as string | undefined;
23
+
24
+ if (!name && !agentId) {
25
+ throw new Error("至少提供 name 或 agent_id 之一");
26
+ }
27
+
28
+ // ── agent_id 查找 ──
29
+ if (agentId) {
30
+ const contacts = readContacts();
31
+ const local = contacts.find(c => c.agent_id === agentId);
32
+ if (local) {
33
+ return ok({ source: "local_contact", results: [formatContact(local)] });
34
+ }
35
+ try {
36
+ const agent = await api.getAgent(agentId);
37
+ return ok({ source: "server", results: [agent] });
38
+ } catch {
39
+ return notFound(`未找到 ID 为 ${agentId} 的用户`);
40
+ }
41
+ }
42
+
43
+ // ── 名字查找 ──
44
+ // 1. 先查本地通讯录
45
+ let localMatches = lookupContactByName(name!);
46
+ if (interest && localMatches.length > 1) {
47
+ const kw = interest.toLowerCase();
48
+ const filtered = localMatches.filter(c =>
49
+ c.topic_tags?.some(t => t.toLowerCase().includes(kw)) ||
50
+ c.auto_bio?.toLowerCase().includes(kw)
51
+ );
52
+ if (filtered.length > 0) localMatches = filtered;
53
+ }
54
+
55
+ // 2. 查服务端(带 intent 做语义排序)
56
+ let serverResults: Record<string, unknown>[] = [];
57
+ try {
58
+ const res = await api.searchByName(name!, interest);
59
+ serverResults = (res.candidates || []).map(c => ({
60
+ agent_id: c.agent_id,
61
+ public_name: c.public_name,
62
+ topic_tags: c.topic_tags,
63
+ availability: c.availability,
64
+ manual_intro: c.manual_intro || "",
65
+ auto_bio: c.auto_bio || "",
66
+ match_reason: c.match_reason || "名字匹配",
67
+ }));
68
+ } catch { /* 服务端不可达时依赖本地结果 */ }
69
+
70
+ // 3. 合并去重(本地优先)
71
+ const localIds = new Set(localMatches.map(c => c.agent_id));
72
+ const merged = [
73
+ ...localMatches.map(formatContact),
74
+ ...serverResults.filter(c => !localIds.has(c.agent_id as string)),
75
+ ];
76
+
77
+ if (merged.length === 0) {
78
+ return notFound(`未找到名字包含"${name}"的用户`);
79
+ }
80
+
81
+ return ok({ results: merged, total: merged.length });
82
+ },
83
+ } as AnyAgentTool;
84
+ }
85
+
86
+ function formatContact(c: { name: string; agent_id: string; session_id?: string; topic_tags?: string[]; auto_bio?: string }) {
87
+ return {
88
+ agent_id: c.agent_id,
89
+ public_name: c.name,
90
+ session_id: c.session_id,
91
+ topic_tags: c.topic_tags || [],
92
+ auto_bio: c.auto_bio || "",
93
+ is_contact: true,
94
+ };
95
+ }
96
+
97
+ function ok(data: Record<string, unknown>) {
98
+ return { content: [{ type: "text", text: JSON.stringify({ found: true, ...data }) }] };
99
+ }
100
+
101
+ function notFound(message: string) {
102
+ return { content: [{ type: "text", text: JSON.stringify({ found: false, message }) }] };
103
+ }
@@ -0,0 +1,54 @@
1
+ import { Type } from "@sinclair/typebox";
2
+ import type { AnyAgentTool } from "../types.js";
3
+ import api from "../api.js";
4
+
5
+ export function createMatchTool(): AnyAgentTool {
6
+ return {
7
+ name: "clawsocial_match",
8
+ label: "ClawSocial 兴趣匹配",
9
+ description:
10
+ "Discover agents by interest or topic using semantic search. " +
11
+ "Use when the user describes characteristics or interests (e.g. '找做AI的人', '找喜欢写作的人'). " +
12
+ "For finding a specific person by name, use clawsocial_find instead. " +
13
+ "Always show results to the user and get explicit approval before connecting.",
14
+ parameters: Type.Object({
15
+ interest: Type.String({ description: "用自然语言描述想找什么样的人或话题" }),
16
+ top_k: Type.Optional(Type.Number({ description: "返回数量,默认 5", minimum: 1, maximum: 20 })),
17
+ }),
18
+ async execute(_id: string, params: Record<string, unknown>) {
19
+ const interest = params.interest as string;
20
+ if (!interest) throw new Error("interest 不能为空");
21
+
22
+ const res = await api.search({
23
+ intent: interest,
24
+ topic_tags: [],
25
+ top_k: (params.top_k as number) ?? 5,
26
+ });
27
+
28
+ if (!res.candidates || res.candidates.length === 0) {
29
+ return {
30
+ content: [{ type: "text", text: JSON.stringify({
31
+ candidates: [],
32
+ message: "暂时没有找到匹配的龙虾。可以稍后再试,或者换一个话题描述。",
33
+ })}],
34
+ };
35
+ }
36
+
37
+ const result = {
38
+ candidates: res.candidates.map(c => ({
39
+ agent_id: c.agent_id,
40
+ public_name: c.public_name,
41
+ topic_tags: c.topic_tags,
42
+ match_score: Math.round(c.match_score * 100) + "%",
43
+ availability: c.availability,
44
+ ...(c.manual_intro ? { manual_intro: c.manual_intro } : {}),
45
+ ...(c.auto_bio ? { auto_bio: c.auto_bio } : {}),
46
+ ...(c.match_reason ? { match_reason: c.match_reason } : {}),
47
+ })),
48
+ total: res.candidates.length,
49
+ query_intent: interest,
50
+ };
51
+ return { content: [{ type: "text", text: JSON.stringify(result) }] };
52
+ },
53
+ } as AnyAgentTool;
54
+ }
@@ -1,35 +0,0 @@
1
- import { Type } from "@sinclair/typebox";
2
- import type { AnyAgentTool } from "../types.js";
3
- import { lookupContactByName } from "../store.js";
4
-
5
- export function createLookupContactTool(): AnyAgentTool {
6
- return {
7
- name: "clawsocial_lookup_contact",
8
- label: "ClawSocial 查找本地联系人",
9
- description:
10
- "Search local contacts by name. Call this FIRST when the user mentions a specific person by name, before calling clawsocial_search. Returns agent_id and session_id if found locally.",
11
- parameters: Type.Object({
12
- name: Type.String({ description: "要查找的联系人名字(支持部分匹配)" }),
13
- }),
14
- async execute(_id: string, params: Record<string, unknown>) {
15
- const name = params.name as string;
16
- const matches = lookupContactByName(name);
17
-
18
- if (matches.length === 0) {
19
- return {
20
- content: [{ type: "text", text: JSON.stringify({
21
- found: false,
22
- message: "本地通讯录未找到此人,请使用 clawsocial_search 搜索",
23
- })}],
24
- };
25
- }
26
-
27
- return {
28
- content: [{ type: "text", text: JSON.stringify({
29
- found: true,
30
- contacts: matches,
31
- })}],
32
- };
33
- },
34
- } as AnyAgentTool;
35
- }
@@ -1,51 +0,0 @@
1
- import { Type } from "@sinclair/typebox";
2
- import type { AnyAgentTool } from "../types.js";
3
- import api from "../api.js";
4
-
5
- export function createSearchTool(): AnyAgentTool {
6
- return {
7
- name: "clawsocial_search",
8
- label: "ClawSocial 搜索",
9
- description:
10
- "Search for agents by interest or topic (semantic search). Use ONLY when the user describes characteristics or interests (e.g. '找做AI的人', '找喜欢写作的人'). Do NOT use this when the user names a specific person — use clawsocial_search_by_name instead. Always show results to the user and get explicit approval before connecting.",
11
- parameters: Type.Object({
12
- intent: Type.String({ description: "用自然语言描述想找什么样的人或话题" }),
13
- topic_tags: Type.Optional(Type.Array(Type.String(), { description: "额外标签,提高相关性" })),
14
- top_k: Type.Optional(Type.Number({ description: "返回数量,默认 5", minimum: 1, maximum: 20 })),
15
- }),
16
- async execute(_id: string, params: Record<string, unknown>) {
17
- const intent = params.intent as string;
18
- if (!intent) throw new Error("intent 不能为空");
19
-
20
- const res = await api.search({
21
- intent,
22
- topic_tags: (params.topic_tags as string[]) ?? [],
23
- top_k: (params.top_k as number) ?? 5,
24
- });
25
-
26
- if (!res.candidates || res.candidates.length === 0) {
27
- const result = {
28
- candidates: [],
29
- message: "暂时没有找到匹配的龙虾。可以稍后再试,或者换一个话题描述。",
30
- };
31
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
32
- }
33
-
34
- const result = {
35
- candidates: res.candidates.map((c) => ({
36
- agent_id: c.agent_id,
37
- public_name: c.public_name,
38
- topic_tags: c.topic_tags,
39
- match_score: Math.round(c.match_score * 100) + "%",
40
- availability: c.availability,
41
- ...(c.manual_intro ? { manual_intro: c.manual_intro } : {}),
42
- ...(c.auto_bio ? { auto_bio: c.auto_bio } : {}),
43
- ...(c.match_reason ? { match_reason: c.match_reason } : {}),
44
- })),
45
- total: res.candidates.length,
46
- query_intent: intent,
47
- };
48
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
49
- },
50
- } as AnyAgentTool;
51
- }
@@ -1,44 +0,0 @@
1
- import { Type } from "@sinclair/typebox";
2
- import type { AnyAgentTool } from "../types.js";
3
- import api from "../api.js";
4
-
5
- export function createSearchByNameTool(): AnyAgentTool {
6
- return {
7
- name: "clawsocial_search_by_name",
8
- label: "ClawSocial 按名字搜索",
9
- description:
10
- "Search for a specific person by name. Use this when the user mentions someone by name (e.g. '找虾杰伦', '联系小明'). Do NOT use clawsocial_search (interest search) for this case. Check local contacts first via clawsocial_lookup_contact before calling this.",
11
- parameters: Type.Object({
12
- name: Type.String({ description: "要搜索的名字(支持部分匹配)" }),
13
- }),
14
- async execute(_id: string, params: Record<string, unknown>) {
15
- const name = params.name as string;
16
- if (!name) throw new Error("name 不能为空");
17
-
18
- const res = await api.searchByName(name);
19
-
20
- if (!res.candidates || res.candidates.length === 0) {
21
- return {
22
- content: [{ type: "text", text: JSON.stringify({
23
- candidates: [],
24
- message: `未找到名字包含"${name}"的用户。`,
25
- })}],
26
- };
27
- }
28
-
29
- const result = {
30
- candidates: res.candidates.map((c) => ({
31
- agent_id: c.agent_id,
32
- public_name: c.public_name,
33
- topic_tags: c.topic_tags,
34
- availability: c.availability,
35
- ...(c.manual_intro ? { manual_intro: c.manual_intro } : {}),
36
- ...(c.auto_bio ? { auto_bio: c.auto_bio } : {}),
37
- match_reason: c.match_reason,
38
- })),
39
- total: res.candidates.length,
40
- };
41
- return { content: [{ type: "text", text: JSON.stringify(result) }] };
42
- },
43
- } as AnyAgentTool;
44
- }