@clawling/clawchat-plugin-openclaw 2026.5.12-28
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/INSTALL.md +64 -0
- package/README.md +227 -0
- package/dist/index.js +20 -0
- package/dist/setup-entry.js +3 -0
- package/dist/src/api-client.js +263 -0
- package/dist/src/api-types.js +17 -0
- package/dist/src/api-types.test-d.js +10 -0
- package/dist/src/buffered-stream.js +177 -0
- package/dist/src/channel.js +66 -0
- package/dist/src/channel.setup.js +119 -0
- package/dist/src/clawchat-memory.js +403 -0
- package/dist/src/clawchat-metadata.js +310 -0
- package/dist/src/client.js +35 -0
- package/dist/src/commands.js +35 -0
- package/dist/src/config.js +274 -0
- package/dist/src/group-message-coalescer.js +119 -0
- package/dist/src/inbound.js +170 -0
- package/dist/src/llm-context-debug.js +86 -0
- package/dist/src/login.runtime.js +204 -0
- package/dist/src/media-runtime.js +85 -0
- package/dist/src/message-mapper.js +146 -0
- package/dist/src/mock-transport.js +31 -0
- package/dist/src/outbound.js +628 -0
- package/dist/src/plugin-prompts.js +89 -0
- package/dist/src/profile-prompt.js +269 -0
- package/dist/src/profile-sync.js +110 -0
- package/dist/src/prompt-injection.js +25 -0
- package/dist/src/protocol-types.js +63 -0
- package/dist/src/protocol-types.typecheck.js +1 -0
- package/dist/src/protocol.js +33 -0
- package/dist/src/reply-dispatcher.js +422 -0
- package/dist/src/runtime.js +1254 -0
- package/dist/src/storage.js +525 -0
- package/dist/src/streaming.js +65 -0
- package/dist/src/terminal-send.js +36 -0
- package/dist/src/tools-schema.js +208 -0
- package/dist/src/tools.js +920 -0
- package/dist/src/ws-alignment.js +178 -0
- package/dist/src/ws-client.js +588 -0
- package/dist/src/ws-log.js +19 -0
- package/index.ts +24 -0
- package/openclaw.plugin.json +169 -0
- package/package.json +80 -0
- package/prompts/default-group-bio.md +19 -0
- package/prompts/default-owner-behavior.md +27 -0
- package/prompts/platform.md +13 -0
- package/setup-entry.ts +4 -0
- package/skills/clawchat/SKILL.md +91 -0
- package/src/api-client.test.ts +827 -0
- package/src/api-client.ts +414 -0
- package/src/api-types.ts +146 -0
- package/src/channel.outbound.test.ts +433 -0
- package/src/channel.setup.ts +145 -0
- package/src/channel.test.ts +262 -0
- package/src/channel.ts +81 -0
- package/src/clawchat-memory.test.ts +480 -0
- package/src/clawchat-memory.ts +533 -0
- package/src/clawchat-metadata.test.ts +477 -0
- package/src/clawchat-metadata.ts +429 -0
- package/src/client.test.ts +169 -0
- package/src/client.ts +56 -0
- package/src/commands.test.ts +39 -0
- package/src/commands.ts +41 -0
- package/src/config.test.ts +344 -0
- package/src/config.ts +404 -0
- package/src/group-message-coalescer.test.ts +237 -0
- package/src/group-message-coalescer.ts +171 -0
- package/src/inbound.test.ts +508 -0
- package/src/inbound.ts +278 -0
- package/src/llm-context-debug.test.ts +55 -0
- package/src/llm-context-debug.ts +139 -0
- package/src/login.runtime.test.ts +737 -0
- package/src/login.runtime.ts +277 -0
- package/src/manifest.test.ts +352 -0
- package/src/media-runtime.test.ts +207 -0
- package/src/media-runtime.ts +152 -0
- package/src/message-mapper.test.ts +201 -0
- package/src/message-mapper.ts +174 -0
- package/src/mock-transport.test.ts +35 -0
- package/src/mock-transport.ts +38 -0
- package/src/outbound.test.ts +1269 -0
- package/src/outbound.ts +803 -0
- package/src/plugin-entry.test.ts +38 -0
- package/src/plugin-prompts.test.ts +94 -0
- package/src/plugin-prompts.ts +107 -0
- package/src/profile-prompt.test.ts +274 -0
- package/src/profile-prompt.ts +351 -0
- package/src/profile-sync.test.ts +539 -0
- package/src/profile-sync.ts +191 -0
- package/src/prompt-injection.test.ts +39 -0
- package/src/prompt-injection.ts +45 -0
- package/src/protocol-types.test.ts +69 -0
- package/src/protocol-types.ts +296 -0
- package/src/protocol-types.typecheck.ts +89 -0
- package/src/protocol.test.ts +39 -0
- package/src/protocol.ts +42 -0
- package/src/reply-dispatcher.test.ts +1324 -0
- package/src/reply-dispatcher.ts +555 -0
- package/src/runtime.test.ts +4719 -0
- package/src/runtime.ts +1493 -0
- package/src/scripts.test.ts +85 -0
- package/src/storage.test.ts +560 -0
- package/src/storage.ts +807 -0
- package/src/terminal-send.test.ts +81 -0
- package/src/terminal-send.ts +56 -0
- package/src/tools-schema.ts +337 -0
- package/src/tools.test.ts +933 -0
- package/src/tools.ts +1185 -0
- package/src/ws-alignment.test.ts +103 -0
- package/src/ws-alignment.ts +275 -0
- package/src/ws-client.test.ts +1217 -0
- package/src/ws-client.ts +662 -0
- package/src/ws-log.test.ts +32 -0
- package/src/ws-log.ts +31 -0
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ClawlingApiError,
|
|
3
|
+
type AgentMetadataPatch,
|
|
4
|
+
type AgentProfile,
|
|
5
|
+
type AgentConnectResult,
|
|
6
|
+
type AvatarUploadResult,
|
|
7
|
+
type ConversationDetails,
|
|
8
|
+
type ConversationMetadataPatch,
|
|
9
|
+
type FriendList,
|
|
10
|
+
type MomentComment,
|
|
11
|
+
type MomentReaction,
|
|
12
|
+
type MomentView,
|
|
13
|
+
type Profile,
|
|
14
|
+
type UploadResult,
|
|
15
|
+
type UserSearchHit,
|
|
16
|
+
} from "./api-types.ts";
|
|
17
|
+
import { CHANNEL_ID } from "./config.ts";
|
|
18
|
+
|
|
19
|
+
export interface ApiClientOptions {
|
|
20
|
+
baseUrl: string;
|
|
21
|
+
token: string;
|
|
22
|
+
/** Logged-in agent's `user_id`, used by WebSocket setup but not REST `/me` calls. */
|
|
23
|
+
userId?: string;
|
|
24
|
+
/** Test override only. Defaults to global `fetch`. */
|
|
25
|
+
fetchImpl?: typeof fetch;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface OpenclawClawlingApiClient {
|
|
29
|
+
getMyProfile(): Promise<Profile>;
|
|
30
|
+
getAgentProfile(agentId: string): Promise<{ agent: AgentProfile }>;
|
|
31
|
+
getAgentDetail(agentId: string): Promise<{ agent: AgentProfile }>;
|
|
32
|
+
patchAgent(agentId: string, patch: AgentMetadataPatch): Promise<{ agent: AgentProfile }>;
|
|
33
|
+
updateAgentBehavior(behavior: string): Promise<{ agent: AgentProfile }>;
|
|
34
|
+
getUserInfo(userId: string): Promise<Profile>;
|
|
35
|
+
getUserProfile(userId: string): Promise<Profile>;
|
|
36
|
+
listFriends(): Promise<FriendList>;
|
|
37
|
+
searchUsers(params: { q?: string; limit?: number }): Promise<{ users: UserSearchHit[] }>;
|
|
38
|
+
listMoments(params: { before?: number; limit?: number }): Promise<{ moments: MomentView[] }>;
|
|
39
|
+
createMoment(body: { text?: string; images?: string[] }): Promise<{ moment: MomentView }>;
|
|
40
|
+
deleteMoment(momentId: number): Promise<{ ok: boolean }>;
|
|
41
|
+
toggleMomentReaction(params: {
|
|
42
|
+
momentId: number;
|
|
43
|
+
emoji: string;
|
|
44
|
+
}): Promise<{ reactions: MomentReaction[] }>;
|
|
45
|
+
createMomentComment(params: {
|
|
46
|
+
momentId: number;
|
|
47
|
+
text: string;
|
|
48
|
+
}): Promise<{ comment: MomentComment }>;
|
|
49
|
+
replyMomentComment(params: {
|
|
50
|
+
momentId: number;
|
|
51
|
+
replyToCommentId: number;
|
|
52
|
+
text: string;
|
|
53
|
+
}): Promise<{ comment: MomentComment }>;
|
|
54
|
+
deleteMomentComment(params: { momentId: number; commentId: number }): Promise<{ ok: boolean }>;
|
|
55
|
+
getConversation(conversationId: string): Promise<{ conversation: ConversationDetails }>;
|
|
56
|
+
patchConversation(
|
|
57
|
+
conversationId: string,
|
|
58
|
+
patch: ConversationMetadataPatch,
|
|
59
|
+
): Promise<{ conversation: ConversationDetails }>;
|
|
60
|
+
updateMyProfile(patch: { nickname?: string; avatar_url?: string; bio?: string }): Promise<Profile>;
|
|
61
|
+
uploadMedia(params: { buffer: Buffer; filename: string; mime?: string }): Promise<UploadResult>;
|
|
62
|
+
/**
|
|
63
|
+
* Exchange an invite code for an agent token.
|
|
64
|
+
* Request body shape: `{ code, platform, type }`.
|
|
65
|
+
*/
|
|
66
|
+
agentsConnect(params: {
|
|
67
|
+
/** The invite code entered by the operator. */
|
|
68
|
+
code: string;
|
|
69
|
+
/** Platform the agent is attaching from (e.g. "openclaw"). */
|
|
70
|
+
platform: string;
|
|
71
|
+
/** Agent type tag (e.g. "bot"). */
|
|
72
|
+
type: string;
|
|
73
|
+
}): Promise<AgentConnectResult>;
|
|
74
|
+
/**
|
|
75
|
+
* Upload an avatar image via `POST /v1/files/upload-url`. The resulting
|
|
76
|
+
* `url` is what you then pass to `updateMyProfile({ avatar_url: url })`.
|
|
77
|
+
*/
|
|
78
|
+
uploadAvatar(params: {
|
|
79
|
+
buffer: Buffer;
|
|
80
|
+
filename: string;
|
|
81
|
+
mime?: string;
|
|
82
|
+
}): Promise<AvatarUploadResult>;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export function createOpenclawClawlingApiClient(opts: ApiClientOptions): OpenclawClawlingApiClient {
|
|
86
|
+
if (!/^https?:\/\//i.test(opts.baseUrl)) {
|
|
87
|
+
throw new ClawlingApiError(
|
|
88
|
+
"validation",
|
|
89
|
+
`clawchat-plugin-openclaw baseUrl must start with http:// or https:// (got "${opts.baseUrl}")`,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
const baseUrl = opts.baseUrl.replace(/\/+$/, "");
|
|
93
|
+
const fetchImpl = opts.fetchImpl ?? globalThis.fetch.bind(globalThis);
|
|
94
|
+
|
|
95
|
+
function url(path: string): string {
|
|
96
|
+
return `${baseUrl}${path}`;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function authHeaders(extra: Record<string, string> = {}): Record<string, string> {
|
|
100
|
+
// `X-Device-Id` is sent on every request so the server can correlate
|
|
101
|
+
// activity back to this plugin instance. Callers may override via
|
|
102
|
+
// `extra` (e.g. tests) but the default is the channel id.
|
|
103
|
+
return {
|
|
104
|
+
authorization: `Bearer ${opts.token}`,
|
|
105
|
+
"x-device-id": CHANNEL_ID,
|
|
106
|
+
...extra,
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function readEnvelope<T>(res: Response, path: string): Promise<T> {
|
|
111
|
+
if (res.status === 401 || res.status === 403) {
|
|
112
|
+
throw new ClawlingApiError("auth", `unauthorized (status ${res.status})`, {
|
|
113
|
+
status: res.status,
|
|
114
|
+
path,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
if (!res.ok) {
|
|
118
|
+
const snippet = await res.text().catch(() => "");
|
|
119
|
+
throw new ClawlingApiError("transport", `http ${res.status} ${snippet.slice(0, 200)}`, {
|
|
120
|
+
status: res.status,
|
|
121
|
+
path,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
let parsed: unknown;
|
|
125
|
+
try {
|
|
126
|
+
parsed = await res.json();
|
|
127
|
+
} catch (err) {
|
|
128
|
+
throw new ClawlingApiError(
|
|
129
|
+
"transport",
|
|
130
|
+
`non-JSON response: ${err instanceof Error ? err.message : String(err)}`,
|
|
131
|
+
{ status: res.status, path },
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
// Unified envelope: `{ code: number, msg: string, data: T }`.
|
|
135
|
+
// `code === 0` means success; any other value is a business error whose
|
|
136
|
+
// `msg` is surfaced to callers and `code` is preserved on the error meta.
|
|
137
|
+
const env = parsed as { code?: unknown; msg?: unknown; message?: unknown; data?: T };
|
|
138
|
+
const code = typeof env.code === "number" ? env.code : Number.NaN;
|
|
139
|
+
const msg =
|
|
140
|
+
typeof env.msg === "string"
|
|
141
|
+
? env.msg
|
|
142
|
+
: typeof env.message === "string"
|
|
143
|
+
? env.message
|
|
144
|
+
: "";
|
|
145
|
+
if (!Number.isFinite(code)) {
|
|
146
|
+
throw new ClawlingApiError("transport", "invalid envelope: missing numeric `code`", {
|
|
147
|
+
status: res.status,
|
|
148
|
+
path,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
if (code !== 0) {
|
|
152
|
+
throw new ClawlingApiError("api", msg || `code=${code}`, {
|
|
153
|
+
code,
|
|
154
|
+
status: res.status,
|
|
155
|
+
path,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
return env.data as T;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
async function call<T>(
|
|
162
|
+
method: string,
|
|
163
|
+
path: string,
|
|
164
|
+
init?: { body?: unknown; headers?: Record<string, string> },
|
|
165
|
+
): Promise<T> {
|
|
166
|
+
let res: Response;
|
|
167
|
+
try {
|
|
168
|
+
const requestInit: RequestInit = {
|
|
169
|
+
method,
|
|
170
|
+
headers: authHeaders(init?.headers),
|
|
171
|
+
};
|
|
172
|
+
if (init?.body !== undefined) {
|
|
173
|
+
requestInit.body = init.body as BodyInit;
|
|
174
|
+
}
|
|
175
|
+
res = await fetchImpl(url(path), requestInit);
|
|
176
|
+
} catch (err) {
|
|
177
|
+
throw new ClawlingApiError(
|
|
178
|
+
"transport",
|
|
179
|
+
`fetch failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
180
|
+
{ path },
|
|
181
|
+
);
|
|
182
|
+
}
|
|
183
|
+
return await readEnvelope<T>(res, path);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function parseUploadResult(data: unknown, path: string): UploadResult {
|
|
187
|
+
const obj = data as Partial<UploadResult> | null;
|
|
188
|
+
const validKind =
|
|
189
|
+
obj?.kind === "image" || obj?.kind === "file" || obj?.kind === "audio" || obj?.kind === "video";
|
|
190
|
+
if (
|
|
191
|
+
!obj ||
|
|
192
|
+
!validKind ||
|
|
193
|
+
typeof obj.url !== "string" ||
|
|
194
|
+
typeof obj.name !== "string" ||
|
|
195
|
+
typeof obj.mime !== "string" ||
|
|
196
|
+
typeof obj.size !== "number"
|
|
197
|
+
) {
|
|
198
|
+
throw new ClawlingApiError(
|
|
199
|
+
"api",
|
|
200
|
+
"invalid upload response: missing required media fields",
|
|
201
|
+
{ path },
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
return obj as UploadResult;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function assertNonBlankId(value: string, label: string): void {
|
|
208
|
+
if (!value.trim()) {
|
|
209
|
+
throw new ClawlingApiError("validation", `${label} is required`);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function pickPatch<T extends object>(
|
|
214
|
+
patch: Record<string, unknown>,
|
|
215
|
+
keys: readonly (keyof T & string)[],
|
|
216
|
+
label: string,
|
|
217
|
+
): T {
|
|
218
|
+
const body: Record<string, unknown> = {};
|
|
219
|
+
for (const key of keys) {
|
|
220
|
+
if (Object.prototype.hasOwnProperty.call(patch, key) && patch[key] !== undefined) {
|
|
221
|
+
body[key] = patch[key];
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (Object.keys(body).length === 0) {
|
|
225
|
+
throw new ClawlingApiError("validation", `${label} patch must include at least one mutable field`);
|
|
226
|
+
}
|
|
227
|
+
return body as T;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// All JSON API endpoints live under `/v1/...`. Media upload is the one
|
|
231
|
+
// intentional exception — the upstream server mounts it at `/media/upload`
|
|
232
|
+
// without the version prefix.
|
|
233
|
+
return {
|
|
234
|
+
async getMyProfile(): Promise<Profile> {
|
|
235
|
+
return await call<Profile>("GET", "/v1/users/me");
|
|
236
|
+
},
|
|
237
|
+
async getAgentProfile(agentId: string): Promise<{ agent: AgentProfile }> {
|
|
238
|
+
return await call<{ agent: AgentProfile }>("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
|
|
239
|
+
},
|
|
240
|
+
async getAgentDetail(agentId: string): Promise<{ agent: AgentProfile }> {
|
|
241
|
+
return await call<{ agent: AgentProfile }>("GET", `/v1/agents/${encodeURIComponent(agentId)}`);
|
|
242
|
+
},
|
|
243
|
+
async patchAgent(agentId: string, patch: AgentMetadataPatch): Promise<{ agent: AgentProfile }> {
|
|
244
|
+
assertNonBlankId(agentId, "patchAgent: agentId");
|
|
245
|
+
const body = pickPatch<AgentMetadataPatch>(
|
|
246
|
+
patch as Record<string, unknown>,
|
|
247
|
+
["nickname", "avatar_url", "bio"],
|
|
248
|
+
"patchAgent",
|
|
249
|
+
);
|
|
250
|
+
return await call<{ agent: AgentProfile }>("PATCH", `/v1/agents/${encodeURIComponent(agentId)}`, {
|
|
251
|
+
body: JSON.stringify(body),
|
|
252
|
+
headers: { "content-type": "application/json" },
|
|
253
|
+
});
|
|
254
|
+
},
|
|
255
|
+
async updateAgentBehavior(behavior: string): Promise<{ agent: AgentProfile }> {
|
|
256
|
+
return await call<{ agent: AgentProfile }>("PATCH", "/v1/agents/me/behavior", {
|
|
257
|
+
body: JSON.stringify({ behavior }),
|
|
258
|
+
headers: { "content-type": "application/json" },
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
async getUserInfo(userId: string): Promise<Profile> {
|
|
262
|
+
return await call<Profile>("GET", `/v1/users/${encodeURIComponent(userId)}`);
|
|
263
|
+
},
|
|
264
|
+
async getUserProfile(userId: string): Promise<Profile> {
|
|
265
|
+
return await call<Profile>("GET", `/v1/users/${encodeURIComponent(userId)}`);
|
|
266
|
+
},
|
|
267
|
+
async listFriends(): Promise<FriendList> {
|
|
268
|
+
return await call<FriendList>("GET", "/v1/friendships");
|
|
269
|
+
},
|
|
270
|
+
async searchUsers(params): Promise<{ users: UserSearchHit[] }> {
|
|
271
|
+
const sp = new URLSearchParams();
|
|
272
|
+
if (typeof params.q === "string") sp.set("q", params.q);
|
|
273
|
+
if (typeof params.limit === "number") sp.set("limit", String(params.limit));
|
|
274
|
+
const q = sp.toString();
|
|
275
|
+
return await call<{ users: UserSearchHit[] }>(
|
|
276
|
+
"GET",
|
|
277
|
+
q ? `/v1/users/search?${q}` : "/v1/users/search",
|
|
278
|
+
);
|
|
279
|
+
},
|
|
280
|
+
async listMoments(params): Promise<{ moments: MomentView[] }> {
|
|
281
|
+
const sp = new URLSearchParams();
|
|
282
|
+
if (typeof params.before === "number") sp.set("before", String(params.before));
|
|
283
|
+
if (typeof params.limit === "number") sp.set("limit", String(params.limit));
|
|
284
|
+
const q = sp.toString();
|
|
285
|
+
return await call<{ moments: MomentView[] }>("GET", q ? `/v1/moments?${q}` : "/v1/moments");
|
|
286
|
+
},
|
|
287
|
+
async createMoment(body): Promise<{ moment: MomentView }> {
|
|
288
|
+
return await call<{ moment: MomentView }>("POST", "/v1/moments", {
|
|
289
|
+
body: JSON.stringify(body),
|
|
290
|
+
headers: { "content-type": "application/json" },
|
|
291
|
+
});
|
|
292
|
+
},
|
|
293
|
+
async deleteMoment(momentId): Promise<{ ok: boolean }> {
|
|
294
|
+
return await call<{ ok: boolean }>("DELETE", `/v1/moments/${encodeURIComponent(String(momentId))}`);
|
|
295
|
+
},
|
|
296
|
+
async toggleMomentReaction(params): Promise<{ reactions: MomentReaction[] }> {
|
|
297
|
+
return await call<{ reactions: MomentReaction[] }>(
|
|
298
|
+
"POST",
|
|
299
|
+
`/v1/moments/${encodeURIComponent(String(params.momentId))}/reactions`,
|
|
300
|
+
{
|
|
301
|
+
body: JSON.stringify({ emoji: params.emoji }),
|
|
302
|
+
headers: { "content-type": "application/json" },
|
|
303
|
+
},
|
|
304
|
+
);
|
|
305
|
+
},
|
|
306
|
+
async createMomentComment(params): Promise<{ comment: MomentComment }> {
|
|
307
|
+
return await call<{ comment: MomentComment }>(
|
|
308
|
+
"POST",
|
|
309
|
+
`/v1/moments/${encodeURIComponent(String(params.momentId))}/comments`,
|
|
310
|
+
{
|
|
311
|
+
body: JSON.stringify({ text: params.text }),
|
|
312
|
+
headers: { "content-type": "application/json" },
|
|
313
|
+
},
|
|
314
|
+
);
|
|
315
|
+
},
|
|
316
|
+
async replyMomentComment(params): Promise<{ comment: MomentComment }> {
|
|
317
|
+
return await call<{ comment: MomentComment }>(
|
|
318
|
+
"POST",
|
|
319
|
+
`/v1/moments/${encodeURIComponent(String(params.momentId))}/comments`,
|
|
320
|
+
{
|
|
321
|
+
body: JSON.stringify({
|
|
322
|
+
text: params.text,
|
|
323
|
+
reply_to_comment_id: params.replyToCommentId,
|
|
324
|
+
}),
|
|
325
|
+
headers: { "content-type": "application/json" },
|
|
326
|
+
},
|
|
327
|
+
);
|
|
328
|
+
},
|
|
329
|
+
async deleteMomentComment(params): Promise<{ ok: boolean }> {
|
|
330
|
+
return await call<{ ok: boolean }>(
|
|
331
|
+
"DELETE",
|
|
332
|
+
`/v1/moments/${encodeURIComponent(String(params.momentId))}/comments/${encodeURIComponent(String(params.commentId))}`,
|
|
333
|
+
);
|
|
334
|
+
},
|
|
335
|
+
async getConversation(conversationId): Promise<{ conversation: ConversationDetails }> {
|
|
336
|
+
return await call<{ conversation: ConversationDetails }>(
|
|
337
|
+
"GET",
|
|
338
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}`,
|
|
339
|
+
);
|
|
340
|
+
},
|
|
341
|
+
async patchConversation(
|
|
342
|
+
conversationId: string,
|
|
343
|
+
patch: ConversationMetadataPatch,
|
|
344
|
+
): Promise<{ conversation: ConversationDetails }> {
|
|
345
|
+
assertNonBlankId(conversationId, "patchConversation: conversationId");
|
|
346
|
+
const body = pickPatch<ConversationMetadataPatch>(
|
|
347
|
+
patch as Record<string, unknown>,
|
|
348
|
+
["title", "description"],
|
|
349
|
+
"patchConversation",
|
|
350
|
+
);
|
|
351
|
+
return await call<{ conversation: ConversationDetails }>(
|
|
352
|
+
"PATCH",
|
|
353
|
+
`/v1/conversations/${encodeURIComponent(conversationId)}`,
|
|
354
|
+
{
|
|
355
|
+
body: JSON.stringify(body),
|
|
356
|
+
headers: { "content-type": "application/json" },
|
|
357
|
+
},
|
|
358
|
+
);
|
|
359
|
+
},
|
|
360
|
+
async updateMyProfile(patch): Promise<Profile> {
|
|
361
|
+
return await call<Profile>(
|
|
362
|
+
"PATCH",
|
|
363
|
+
`/v1/users/me`,
|
|
364
|
+
{
|
|
365
|
+
body: JSON.stringify(patch),
|
|
366
|
+
headers: { "content-type": "application/json" },
|
|
367
|
+
},
|
|
368
|
+
);
|
|
369
|
+
},
|
|
370
|
+
async agentsConnect({ code: inviteCode, platform, type }): Promise<AgentConnectResult> {
|
|
371
|
+
if (!inviteCode?.trim()) {
|
|
372
|
+
throw new ClawlingApiError("validation", "agentsConnect: inviteCode is required");
|
|
373
|
+
}
|
|
374
|
+
if (!platform?.trim()) {
|
|
375
|
+
throw new ClawlingApiError("validation", "agentsConnect: platform is required");
|
|
376
|
+
}
|
|
377
|
+
if (!type?.trim()) {
|
|
378
|
+
throw new ClawlingApiError("validation", "agentsConnect: type is required");
|
|
379
|
+
}
|
|
380
|
+
return await call<AgentConnectResult>("POST", "/v1/agents/connect", {
|
|
381
|
+
// `X-Device-Id` is added globally via `authHeaders` on every request.
|
|
382
|
+
headers: { "content-type": "application/json" },
|
|
383
|
+
body: JSON.stringify({
|
|
384
|
+
code: inviteCode.trim(),
|
|
385
|
+
platform: platform.trim(),
|
|
386
|
+
type: type.trim(),
|
|
387
|
+
}),
|
|
388
|
+
});
|
|
389
|
+
},
|
|
390
|
+
async uploadMedia(params): Promise<UploadResult> {
|
|
391
|
+
const blob = new Blob([new Uint8Array(params.buffer)], {
|
|
392
|
+
type: params.mime ?? "application/octet-stream",
|
|
393
|
+
});
|
|
394
|
+
const file = new File([blob], params.filename, {
|
|
395
|
+
type: params.mime ?? "application/octet-stream",
|
|
396
|
+
});
|
|
397
|
+
const fd = new FormData();
|
|
398
|
+
fd.set("file", file);
|
|
399
|
+
const data = await call<unknown>("POST", "/media/upload", { body: fd });
|
|
400
|
+
return parseUploadResult(data, "/media/upload");
|
|
401
|
+
},
|
|
402
|
+
async uploadAvatar(params): Promise<AvatarUploadResult> {
|
|
403
|
+
const blob = new Blob([new Uint8Array(params.buffer)], {
|
|
404
|
+
type: params.mime ?? "application/octet-stream",
|
|
405
|
+
});
|
|
406
|
+
const file = new File([blob], params.filename, {
|
|
407
|
+
type: params.mime ?? "application/octet-stream",
|
|
408
|
+
});
|
|
409
|
+
const fd = new FormData();
|
|
410
|
+
fd.set("file", file);
|
|
411
|
+
return await call<AvatarUploadResult>("POST", "/v1/files/upload-url", { body: fd });
|
|
412
|
+
},
|
|
413
|
+
};
|
|
414
|
+
}
|
package/src/api-types.ts
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public-facing types for the Clawling Chat HTTP API.
|
|
3
|
+
*
|
|
4
|
+
* Field names mirror the upstream API (snake_case) so we don't lose
|
|
5
|
+
* fidelity on responses. Tool schemas accept camelCase externally and
|
|
6
|
+
* translate at the api-client boundary.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export interface Profile {
|
|
10
|
+
id: string;
|
|
11
|
+
type?: string;
|
|
12
|
+
nickname?: string;
|
|
13
|
+
avatar_url?: string;
|
|
14
|
+
bio?: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface FriendList {
|
|
18
|
+
friends: Profile[];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface UserSearchHit {
|
|
22
|
+
avatar_url?: string;
|
|
23
|
+
id: string;
|
|
24
|
+
nickname?: string;
|
|
25
|
+
type?: string;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface MomentReaction {
|
|
29
|
+
count: number;
|
|
30
|
+
emoji: string;
|
|
31
|
+
mine: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MomentComment {
|
|
35
|
+
author_id: string;
|
|
36
|
+
created_at: string;
|
|
37
|
+
id: number;
|
|
38
|
+
reply_to_author_id?: string;
|
|
39
|
+
reply_to_comment_id?: number;
|
|
40
|
+
text: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface MomentView {
|
|
44
|
+
author_id: string;
|
|
45
|
+
comments?: MomentComment[];
|
|
46
|
+
comments_total?: number;
|
|
47
|
+
created_at: string;
|
|
48
|
+
id: number;
|
|
49
|
+
images?: string[];
|
|
50
|
+
reactions?: MomentReaction[];
|
|
51
|
+
text?: string;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface UploadResult {
|
|
55
|
+
kind: "image" | "file" | "audio" | "video";
|
|
56
|
+
url: string;
|
|
57
|
+
name: string;
|
|
58
|
+
size: number;
|
|
59
|
+
mime: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface AvatarUploadResult {
|
|
63
|
+
url: string;
|
|
64
|
+
size: number;
|
|
65
|
+
mime: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ConversationParticipant {
|
|
69
|
+
conversation_id: string;
|
|
70
|
+
user_id: string;
|
|
71
|
+
role: string;
|
|
72
|
+
joined_at: string;
|
|
73
|
+
[key: string]: unknown;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface ConversationDetails {
|
|
77
|
+
id: string;
|
|
78
|
+
type: string;
|
|
79
|
+
title: string;
|
|
80
|
+
creator_id: string;
|
|
81
|
+
created_at: string;
|
|
82
|
+
updated_at: string;
|
|
83
|
+
participants: ConversationParticipant[];
|
|
84
|
+
[key: string]: unknown;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface ConversationMetadataPatch {
|
|
88
|
+
title?: string;
|
|
89
|
+
description?: string;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export interface AgentProfile {
|
|
93
|
+
id: string;
|
|
94
|
+
owner_id: string;
|
|
95
|
+
user_id: string;
|
|
96
|
+
type: string;
|
|
97
|
+
nickname: string;
|
|
98
|
+
avatar_url: string;
|
|
99
|
+
bio: string;
|
|
100
|
+
behavior?: string;
|
|
101
|
+
visibility: string;
|
|
102
|
+
status: string;
|
|
103
|
+
platform: string;
|
|
104
|
+
created_at: string;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export interface AgentMetadataPatch {
|
|
108
|
+
nickname?: string;
|
|
109
|
+
avatar_url?: string;
|
|
110
|
+
bio?: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export interface AgentConnectConversation {
|
|
114
|
+
id: string;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Response payload for `POST /v1/agents/connect` — the endpoint that exchanges
|
|
119
|
+
* an invite code for the credentials this account uses to open the
|
|
120
|
+
* streaming WebSocket. `access_token` is the bearer this agent uses for
|
|
121
|
+
* API + WebSocket auth; `agent.user_id` is the stable id the streaming
|
|
122
|
+
* protocol routes on.
|
|
123
|
+
*/
|
|
124
|
+
export interface AgentConnectResult {
|
|
125
|
+
agent: AgentProfile;
|
|
126
|
+
access_token: string;
|
|
127
|
+
refresh_token: string;
|
|
128
|
+
conversation?: AgentConnectConversation;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
export type ClawlingApiErrorKind =
|
|
132
|
+
| "auth" // 401 / 403 — token bad or expired
|
|
133
|
+
| "api" // server-returned non-zero `code`
|
|
134
|
+
| "transport" // network failure / 5xx / non-JSON body
|
|
135
|
+
| "validation"; // local pre-check failure (e.g., file too large)
|
|
136
|
+
|
|
137
|
+
export class ClawlingApiError extends Error {
|
|
138
|
+
constructor(
|
|
139
|
+
public readonly kind: ClawlingApiErrorKind,
|
|
140
|
+
message: string,
|
|
141
|
+
public readonly meta?: { code?: number; status?: number; path?: string },
|
|
142
|
+
) {
|
|
143
|
+
super(message);
|
|
144
|
+
this.name = "ClawlingApiError";
|
|
145
|
+
}
|
|
146
|
+
}
|