@firstperson/firstperson 2026.1.37 → 2026.1.39
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/.claude/settings.local.json +4 -1
- package/index.ts +83 -0
- package/package.json +2 -1
- package/src/channel.ts +1 -17
- package/src/relay-client.ts +0 -57
package/index.ts
CHANGED
|
@@ -1,9 +1,19 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
2
2
|
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
|
|
3
|
+
import { Type } from "@sinclair/typebox";
|
|
3
4
|
|
|
4
5
|
import { firstPersonPlugin } from "./src/channel.js";
|
|
5
6
|
import { setFirstPersonRuntime } from "./src/runtime.js";
|
|
6
7
|
|
|
8
|
+
// TypeBox schemas for profile tools
|
|
9
|
+
const UpdateProfileSchema = Type.Object({
|
|
10
|
+
name: Type.String({ description: "Your display name (1-50 characters, required)" }),
|
|
11
|
+
bio: Type.Optional(Type.String({ description: "A short bio or description (up to 160 characters)" })),
|
|
12
|
+
avatar_url: Type.Optional(Type.String({ description: "HTTPS URL to your avatar image" })),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
const GetProfileSchema = Type.Object({});
|
|
16
|
+
|
|
7
17
|
const plugin = {
|
|
8
18
|
id: "firstperson",
|
|
9
19
|
name: "First Person",
|
|
@@ -12,6 +22,79 @@ const plugin = {
|
|
|
12
22
|
register(api: OpenClawPluginApi) {
|
|
13
23
|
setFirstPersonRuntime(api.runtime);
|
|
14
24
|
api.registerChannel({ plugin: firstPersonPlugin });
|
|
25
|
+
|
|
26
|
+
// Profile management tools for bot agency
|
|
27
|
+
api.registerTool((ctx) => {
|
|
28
|
+
const channelConfig = ctx.config?.channels?.firstperson as { token?: string; relayUrl?: string } | undefined;
|
|
29
|
+
const relayToken = channelConfig?.token;
|
|
30
|
+
const relayUrl = channelConfig?.relayUrl ?? "https://chat.firstperson.ai";
|
|
31
|
+
|
|
32
|
+
if (!relayToken) {
|
|
33
|
+
return null; // Tool not available without token
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
name: "firstperson_update_profile",
|
|
38
|
+
description: "Update your profile on the First Person app. Use this to set or change your display name, bio, or avatar that users see in the iOS app.",
|
|
39
|
+
parameters: UpdateProfileSchema,
|
|
40
|
+
execute: async (_toolCallId: string, args: { name: string; bio?: string; avatar_url?: string }) => {
|
|
41
|
+
const { name, bio = "", avatar_url = "" } = args;
|
|
42
|
+
|
|
43
|
+
const res = await fetch(`${relayUrl}/api/v1/profile`, {
|
|
44
|
+
method: "PUT",
|
|
45
|
+
headers: {
|
|
46
|
+
Authorization: `Bearer ${relayToken}`,
|
|
47
|
+
"Content-Type": "application/json",
|
|
48
|
+
},
|
|
49
|
+
body: JSON.stringify({ name, bio, avatar_url }),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
if (!res.ok) {
|
|
53
|
+
const errorText = await res.text().catch(() => "Unknown error");
|
|
54
|
+
return { success: false, error: `Failed to update profile: ${res.status} - ${errorText}` };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const data = await res.json();
|
|
58
|
+
return { success: true, profile: data.profile };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}, { name: "firstperson_update_profile" });
|
|
62
|
+
|
|
63
|
+
api.registerTool((ctx) => {
|
|
64
|
+
const channelConfig = ctx.config?.channels?.firstperson as { token?: string; relayUrl?: string } | undefined;
|
|
65
|
+
const relayToken = channelConfig?.token;
|
|
66
|
+
const relayUrl = channelConfig?.relayUrl ?? "https://chat.firstperson.ai";
|
|
67
|
+
|
|
68
|
+
if (!relayToken) {
|
|
69
|
+
return null; // Tool not available without token
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return {
|
|
73
|
+
name: "firstperson_get_profile",
|
|
74
|
+
description: "Get your current profile from the First Person app. Use this to check what name, bio, and avatar users currently see.",
|
|
75
|
+
parameters: GetProfileSchema,
|
|
76
|
+
execute: async () => {
|
|
77
|
+
const res = await fetch(`${relayUrl}/api/v1/profile`, {
|
|
78
|
+
method: "GET",
|
|
79
|
+
headers: {
|
|
80
|
+
Authorization: `Bearer ${relayToken}`,
|
|
81
|
+
"Content-Type": "application/json",
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (!res.ok) {
|
|
86
|
+
if (res.status === 404) {
|
|
87
|
+
return { success: true, profile: null, message: "No profile set yet" };
|
|
88
|
+
}
|
|
89
|
+
const errorText = await res.text().catch(() => "Unknown error");
|
|
90
|
+
return { success: false, error: `Failed to get profile: ${res.status} - ${errorText}` };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const data = await res.json();
|
|
94
|
+
return { success: true, profile: data.profile };
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
}, { name: "firstperson_get_profile" });
|
|
15
98
|
},
|
|
16
99
|
};
|
|
17
100
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@firstperson/firstperson",
|
|
3
|
-
"version": "2026.1.
|
|
3
|
+
"version": "2026.1.39",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "OpenClaw channel plugin for the First Person iOS app",
|
|
6
6
|
"main": "index.ts",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
}
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
+
"@sinclair/typebox": "^0.34.0",
|
|
42
43
|
"ws": "^8.16.0",
|
|
43
44
|
"zod": "^4.0.0"
|
|
44
45
|
},
|
package/src/channel.ts
CHANGED
|
@@ -318,7 +318,7 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
|
|
|
318
318
|
lastError: null,
|
|
319
319
|
});
|
|
320
320
|
|
|
321
|
-
const { startRelayConnection, sendStatusUpdate
|
|
321
|
+
const { startRelayConnection, sendStatusUpdate } = await import("./relay-client.js");
|
|
322
322
|
|
|
323
323
|
return startRelayConnection({
|
|
324
324
|
relayUrl: account.relayUrl,
|
|
@@ -335,22 +335,6 @@ export const firstPersonPlugin: ChannelPlugin<ResolvedFirstPersonAccount> = {
|
|
|
335
335
|
lastError: null,
|
|
336
336
|
});
|
|
337
337
|
log?.info(`[firstperson:${account.accountId}] relay connected`);
|
|
338
|
-
|
|
339
|
-
// Push agent profile to relay on connection
|
|
340
|
-
const globalRuntime = getFirstPersonRuntime();
|
|
341
|
-
const identity = (globalRuntime as any).agent?.identity?.get?.();
|
|
342
|
-
if (identity) {
|
|
343
|
-
pushProfileToRelay({
|
|
344
|
-
relayUrl: account.relayUrl,
|
|
345
|
-
token: account.token!,
|
|
346
|
-
identity,
|
|
347
|
-
log,
|
|
348
|
-
}).catch((err) => {
|
|
349
|
-
log?.warn(`[firstperson:${account.accountId}] profile push failed: ${err.message}`);
|
|
350
|
-
});
|
|
351
|
-
} else {
|
|
352
|
-
log?.info(`[firstperson:${account.accountId}] no agent identity available, skipping profile push`);
|
|
353
|
-
}
|
|
354
338
|
},
|
|
355
339
|
onDisconnected: (error) => {
|
|
356
340
|
setStatus({
|
package/src/relay-client.ts
CHANGED
|
@@ -1,62 +1,5 @@
|
|
|
1
1
|
import WebSocket from "ws";
|
|
2
2
|
|
|
3
|
-
/**
|
|
4
|
-
* Agent identity from OpenClaw runtime
|
|
5
|
-
*/
|
|
6
|
-
export interface AgentIdentity {
|
|
7
|
-
agentId: string;
|
|
8
|
-
name?: string;
|
|
9
|
-
avatar?: string;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Push the agent's profile to the relay.
|
|
14
|
-
* Called on startup after relay connection is established.
|
|
15
|
-
*/
|
|
16
|
-
export async function pushProfileToRelay(params: {
|
|
17
|
-
relayUrl: string;
|
|
18
|
-
token: string;
|
|
19
|
-
identity: AgentIdentity;
|
|
20
|
-
log?: Logger;
|
|
21
|
-
}): Promise<void> {
|
|
22
|
-
const { relayUrl, token, identity, log } = params;
|
|
23
|
-
|
|
24
|
-
// Convert WebSocket URL to HTTP URL for API call
|
|
25
|
-
const httpUrl = relayUrl.replace(/^wss:\/\//, "https://").replace(/^ws:\/\//, "http://");
|
|
26
|
-
const profileUrl = `${httpUrl}/api/v1/profile`;
|
|
27
|
-
|
|
28
|
-
// Map OpenClaw identity to relay profile format
|
|
29
|
-
// Only include avatar_url if it's actually an HTTPS URL (relay validates this)
|
|
30
|
-
const avatarUrl = identity.avatar?.startsWith("https://") ? identity.avatar : "";
|
|
31
|
-
|
|
32
|
-
const profile = {
|
|
33
|
-
name: identity.name ?? identity.agentId,
|
|
34
|
-
bio: "",
|
|
35
|
-
avatar_url: avatarUrl,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
try {
|
|
39
|
-
const response = await fetch(profileUrl, {
|
|
40
|
-
method: "PUT",
|
|
41
|
-
headers: {
|
|
42
|
-
"Content-Type": "application/json",
|
|
43
|
-
Authorization: `Bearer ${token}`,
|
|
44
|
-
},
|
|
45
|
-
body: JSON.stringify(profile),
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
if (!response.ok) {
|
|
49
|
-
const errorText = await response.text().catch(() => "unknown error");
|
|
50
|
-
log?.warn(`[relay-client] Failed to push profile: ${response.status} ${errorText}`);
|
|
51
|
-
return;
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
log?.info(`[relay-client] Profile pushed to relay: ${profile.name}`);
|
|
55
|
-
} catch (err) {
|
|
56
|
-
log?.warn(`[relay-client] Failed to push profile: ${(err as Error).message}`);
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
3
|
export interface Logger {
|
|
61
4
|
info: (msg: string) => void;
|
|
62
5
|
warn: (msg: string) => void;
|