@yahaha-studio/kichi-forwarder 0.1.2-beta.12 → 0.1.2-beta.14
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 +2 -0
- package/dist/index.js +74 -3
- package/dist/src/service.js +49 -2
- package/index.ts +85 -4
- package/openclaw.plugin.json +2 -1
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +14 -1
- package/src/service.ts +64 -1
- package/src/types.ts +18 -0
package/README.md
CHANGED
|
@@ -12,6 +12,7 @@ It can directly control your companion's avatar in Kichi, show what it is doing,
|
|
|
12
12
|
|
|
13
13
|
- Bring your OpenClaw companion into Kichi
|
|
14
14
|
- Directly control the avatar's poses and actions in Kichi
|
|
15
|
+
- Let the avatar briefly glance at the camera when you ask for attention in chat
|
|
15
16
|
- Keep its visible state in sync while it works
|
|
16
17
|
- Plan human-like idle routines during heartbeat windows
|
|
17
18
|
- Let it leave notes for you in Kichi
|
|
@@ -36,6 +37,7 @@ Get the `host` and `avatarId` from Kichi, then use them with `kichi_switch_host`
|
|
|
36
37
|
|
|
37
38
|
- Connect to your chosen Kichi host and stay in sync while it works
|
|
38
39
|
- Directly control the Kichi avatar's poses and actions
|
|
40
|
+
- Briefly glance toward the camera when you directly ask from chat
|
|
39
41
|
- Show activity in Kichi with actions, bubbles, logs, and timers
|
|
40
42
|
- Leave notes for you on Kichi note boards
|
|
41
43
|
- Recommend music in Kichi as part of your daily routine
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,7 @@ const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
|
38
38
|
const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
|
|
39
39
|
const MAX_AGENT_END_PREVIEW_WIDTH = 10;
|
|
40
40
|
const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
41
|
+
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
41
42
|
const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"];
|
|
42
43
|
let cachedStaticConfig = null;
|
|
43
44
|
let cachedStaticConfigMtime = 0;
|
|
@@ -814,7 +815,7 @@ function buildKichiActionDescription(service) {
|
|
|
814
815
|
const roomContext = service?.getCachedRoomContext();
|
|
815
816
|
const poseableProps = roomContext?.PoseableProps;
|
|
816
817
|
if (Array.isArray(poseableProps) && poseableProps.length > 0) {
|
|
817
|
-
lines.push("", "Cached RoomContext.PoseableProps (from last kichi_query_status):", JSON.stringify(poseableProps), "When using a sit or lay pose, pick the propId whose
|
|
818
|
+
lines.push("", "Cached RoomContext.PoseableProps (from last kichi_query_status):", JSON.stringify(poseableProps), "When using a sit or lay pose, pick the propId whose PoseableProps information best matches the current task context and whose OccupancyState is not fully_occupied. If no prop fits, omit propId.");
|
|
818
819
|
}
|
|
819
820
|
return lines.join("\n");
|
|
820
821
|
}
|
|
@@ -961,6 +962,10 @@ const plugin = {
|
|
|
961
962
|
description: "Optional list of OpenClaw self-perceived personality tags",
|
|
962
963
|
items: { type: "string" },
|
|
963
964
|
},
|
|
965
|
+
source: {
|
|
966
|
+
type: "string",
|
|
967
|
+
description: "Optional join source identifier. Defaults to Kichi World join-source.json, then openclaw.",
|
|
968
|
+
},
|
|
964
969
|
},
|
|
965
970
|
required: ["botName", "bio"],
|
|
966
971
|
},
|
|
@@ -974,6 +979,7 @@ const plugin = {
|
|
|
974
979
|
let avatarId = params?.avatarId;
|
|
975
980
|
const botName = params?.botName?.trim();
|
|
976
981
|
const bio = params?.bio?.trim();
|
|
982
|
+
const rawSource = params?.source;
|
|
977
983
|
const { tags, error: tagsError } = normalizeJoinTags(params?.tags);
|
|
978
984
|
if (!avatarId) {
|
|
979
985
|
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
@@ -987,10 +993,22 @@ const plugin = {
|
|
|
987
993
|
if (!bio) {
|
|
988
994
|
return jsonResult({ success: false, error: "No bio" });
|
|
989
995
|
}
|
|
996
|
+
let source;
|
|
997
|
+
try {
|
|
998
|
+
source = rawSource === undefined
|
|
999
|
+
? service.readConfiguredJoinSource() ?? "openclaw"
|
|
1000
|
+
: trimOptionalString(rawSource);
|
|
1001
|
+
}
|
|
1002
|
+
catch (err) {
|
|
1003
|
+
return jsonResult({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1004
|
+
}
|
|
1005
|
+
if (!source) {
|
|
1006
|
+
return jsonResult({ success: false, error: "source must be a non-empty string" });
|
|
1007
|
+
}
|
|
990
1008
|
if (tagsError) {
|
|
991
1009
|
return jsonResult({ success: false, error: tagsError });
|
|
992
1010
|
}
|
|
993
|
-
const result = await service.join(avatarId, botName, bio, tags ?? []);
|
|
1011
|
+
const result = await service.join(avatarId, botName, bio, tags ?? [], source);
|
|
994
1012
|
if (result.success) {
|
|
995
1013
|
return jsonResult({ success: true, authKey: result.authKey });
|
|
996
1014
|
}
|
|
@@ -1221,6 +1239,59 @@ const plugin = {
|
|
|
1221
1239
|
},
|
|
1222
1240
|
});
|
|
1223
1241
|
}, { name: "kichi_action" });
|
|
1242
|
+
api.registerTool((ctx) => ({
|
|
1243
|
+
name: "kichi_glance",
|
|
1244
|
+
label: "kichi_glance",
|
|
1245
|
+
description: "Ask the Kichi avatar to briefly look at the camera. Use only for direct player chat requests such as \"look at me\" or \"look at the camera\". Do not use for heartbeat, idle planning, bot-to-bot messages, lifecycle hooks, or routine work/status sync.",
|
|
1246
|
+
parameters: {
|
|
1247
|
+
type: "object",
|
|
1248
|
+
properties: {
|
|
1249
|
+
requestId: {
|
|
1250
|
+
type: "string",
|
|
1251
|
+
description: "Optional client request ID for tracing. The websocket ack returns this ID.",
|
|
1252
|
+
},
|
|
1253
|
+
target: {
|
|
1254
|
+
type: "string",
|
|
1255
|
+
enum: ["camera"],
|
|
1256
|
+
description: "Glance target. The only supported target is camera.",
|
|
1257
|
+
},
|
|
1258
|
+
duration: {
|
|
1259
|
+
type: "number",
|
|
1260
|
+
description: "Optional glance duration in seconds. Defaults to 1.8.",
|
|
1261
|
+
},
|
|
1262
|
+
},
|
|
1263
|
+
},
|
|
1264
|
+
execute: async (_toolCallId, params) => {
|
|
1265
|
+
const locator = resolveToolLocator(ctx);
|
|
1266
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1267
|
+
if (!agentId) {
|
|
1268
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1269
|
+
}
|
|
1270
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1271
|
+
const { requestId, target, duration } = (params || {});
|
|
1272
|
+
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1273
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1274
|
+
}
|
|
1275
|
+
const normalizedTarget = target === undefined ? "camera" : target;
|
|
1276
|
+
if (normalizedTarget !== "camera") {
|
|
1277
|
+
return jsonResult({ success: false, error: "target must be camera" });
|
|
1278
|
+
}
|
|
1279
|
+
const normalizedDuration = duration === undefined ? DEFAULT_GLANCE_DURATION_SECONDS : duration;
|
|
1280
|
+
if (typeof normalizedDuration !== "number" || !Number.isFinite(normalizedDuration) || normalizedDuration <= 0) {
|
|
1281
|
+
return jsonResult({ success: false, error: "duration must be a positive finite number" });
|
|
1282
|
+
}
|
|
1283
|
+
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1284
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1285
|
+
}
|
|
1286
|
+
try {
|
|
1287
|
+
const ack = await service.sendGlance("camera", normalizedDuration, typeof requestId === "string" ? requestId : undefined);
|
|
1288
|
+
return jsonResult({ success: true, ...ack });
|
|
1289
|
+
}
|
|
1290
|
+
catch (error) {
|
|
1291
|
+
return jsonResult({ success: false, error: `Failed to send glance: ${error}` });
|
|
1292
|
+
}
|
|
1293
|
+
},
|
|
1294
|
+
}), { name: "kichi_glance" });
|
|
1224
1295
|
api.registerTool((ctx) => ({
|
|
1225
1296
|
name: "kichi_idle_plan",
|
|
1226
1297
|
label: "kichi_idle_plan",
|
|
@@ -1449,7 +1520,7 @@ const plugin = {
|
|
|
1449
1520
|
api.registerTool((ctx) => ({
|
|
1450
1521
|
name: "kichi_query_status",
|
|
1451
1522
|
label: "kichi_query_status",
|
|
1452
|
-
description: "Query Kichi room and avatar status — includes room personnel, notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota, `hasCreatedMusicAlbumToday`, and RoomContext.PoseableProps (poseable props with PropId, DisplayName, SupportedPoseTypes, OccupancyState). The PoseableProps list is cached internally so that kichi_action can reference a propId during regular work sync without re-querying. Use this when the user asks to check kichi status, room status, or who is in the room. Also use this before creating a new note or daily recommended music album. For heartbeat planning, use the returned idlePlan as reference when shaping the next idle plan.",
|
|
1523
|
+
description: "Query Kichi room and avatar status — includes room personnel, notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota, `hasCreatedMusicAlbumToday`, and RoomContext.PoseableProps (poseable props with PropId, DisplayName, Description, SupportedPoseTypes, OccupancyState). The PoseableProps list is cached internally so that kichi_action can reference a propId during regular work sync without re-querying. Use this when the user asks to check kichi status, room status, or who is in the room. Also use this before creating a new note or daily recommended music album. For heartbeat planning, use the returned idlePlan as reference when shaping the next idle plan.",
|
|
1453
1524
|
parameters: {
|
|
1454
1525
|
type: "object",
|
|
1455
1526
|
properties: {
|
package/dist/src/service.js
CHANGED
|
@@ -4,6 +4,8 @@ import * as path from "path";
|
|
|
4
4
|
import { randomUUID } from "node:crypto";
|
|
5
5
|
const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
6
6
|
const DEFAULT_LLM_RUNTIME_ENABLED = true;
|
|
7
|
+
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
8
|
+
const JOIN_SOURCE_FILE_NAME = "join-source.json";
|
|
7
9
|
export class KichiForwarderService {
|
|
8
10
|
logger;
|
|
9
11
|
options;
|
|
@@ -63,7 +65,7 @@ export class KichiForwarderService {
|
|
|
63
65
|
}
|
|
64
66
|
return this.getConnectionStatus();
|
|
65
67
|
}
|
|
66
|
-
async join(avatarId, botName, bio, tags) {
|
|
68
|
+
async join(avatarId, botName, bio, tags, source) {
|
|
67
69
|
if (!this.host) {
|
|
68
70
|
return { success: false, error: "No Kichi host configured. Run kichi_switch_host first." };
|
|
69
71
|
}
|
|
@@ -78,7 +80,7 @@ export class KichiForwarderService {
|
|
|
78
80
|
this.identity = { avatarId };
|
|
79
81
|
this.saveIdentity();
|
|
80
82
|
this.joinResolve = resolve;
|
|
81
|
-
const payload = { type: "join", avatarId, botName, bio, tags };
|
|
83
|
+
const payload = { type: "join", avatarId, botName, bio, tags, source };
|
|
82
84
|
const sendJoin = () => this.ws?.send(JSON.stringify(payload));
|
|
83
85
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
84
86
|
sendJoin();
|
|
@@ -176,6 +178,30 @@ export class KichiForwarderService {
|
|
|
176
178
|
this.ws.send(JSON.stringify(payload));
|
|
177
179
|
return true;
|
|
178
180
|
}
|
|
181
|
+
async sendGlance(target, durationSeconds = DEFAULT_GLANCE_DURATION_SECONDS, requestId) {
|
|
182
|
+
const identity = this.requireIdentity();
|
|
183
|
+
if (!identity) {
|
|
184
|
+
throw new Error("Missing Kichi identity");
|
|
185
|
+
}
|
|
186
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
187
|
+
throw new Error("Kichi websocket is not connected");
|
|
188
|
+
}
|
|
189
|
+
if (target !== "camera") {
|
|
190
|
+
throw new Error("target must be camera");
|
|
191
|
+
}
|
|
192
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
|
|
193
|
+
throw new Error("duration must be a positive finite number");
|
|
194
|
+
}
|
|
195
|
+
const payload = {
|
|
196
|
+
type: "kichi_glance",
|
|
197
|
+
requestId: requestId?.trim() || randomUUID(),
|
|
198
|
+
avatarId: identity.avatarId,
|
|
199
|
+
authKey: identity.authKey,
|
|
200
|
+
target,
|
|
201
|
+
duration: durationSeconds,
|
|
202
|
+
};
|
|
203
|
+
return this.sendRequest(payload, "kichi_glance_ack", 5000);
|
|
204
|
+
}
|
|
179
205
|
async queryStatus(requestId) {
|
|
180
206
|
const identity = this.requireIdentity();
|
|
181
207
|
if (!identity) {
|
|
@@ -274,6 +300,24 @@ export class KichiForwarderService {
|
|
|
274
300
|
getRuntimeDir() {
|
|
275
301
|
return this.options.runtimeDir;
|
|
276
302
|
}
|
|
303
|
+
getJoinSourcePath() {
|
|
304
|
+
return path.join(this.getKichiWorldRootDir(), JOIN_SOURCE_FILE_NAME);
|
|
305
|
+
}
|
|
306
|
+
readConfiguredJoinSource() {
|
|
307
|
+
const sourcePath = this.getJoinSourcePath();
|
|
308
|
+
if (!fs.existsSync(sourcePath)) {
|
|
309
|
+
return null;
|
|
310
|
+
}
|
|
311
|
+
const data = JSON.parse(fs.readFileSync(sourcePath, "utf-8"));
|
|
312
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
313
|
+
throw new Error(`${JOIN_SOURCE_FILE_NAME} must contain a JSON object`);
|
|
314
|
+
}
|
|
315
|
+
const source = data.source;
|
|
316
|
+
if (typeof source !== "string" || !source.trim()) {
|
|
317
|
+
throw new Error(`${JOIN_SOURCE_FILE_NAME} must contain a non-empty string source`);
|
|
318
|
+
}
|
|
319
|
+
return source.trim();
|
|
320
|
+
}
|
|
277
321
|
getStatePath() {
|
|
278
322
|
return path.join(this.options.runtimeDir, "state.json");
|
|
279
323
|
}
|
|
@@ -621,6 +665,9 @@ export class KichiForwarderService {
|
|
|
621
665
|
}
|
|
622
666
|
return path.join(this.options.runtimeDir, "hosts", encodeURIComponent(this.host));
|
|
623
667
|
}
|
|
668
|
+
getKichiWorldRootDir() {
|
|
669
|
+
return path.dirname(path.dirname(this.options.runtimeDir));
|
|
670
|
+
}
|
|
624
671
|
getWsUrl() {
|
|
625
672
|
if (!this.host) {
|
|
626
673
|
throw new Error("No Kichi host configured");
|
package/index.ts
CHANGED
|
@@ -60,6 +60,7 @@ const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
|
60
60
|
const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
|
|
61
61
|
const MAX_AGENT_END_PREVIEW_WIDTH = 10;
|
|
62
62
|
const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
63
|
+
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
63
64
|
const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"] as const;
|
|
64
65
|
let cachedStaticConfig: KichiStaticConfig | null = null;
|
|
65
66
|
let cachedStaticConfigMtime = 0;
|
|
@@ -1014,7 +1015,7 @@ function buildKichiActionDescription(service?: KichiForwarderService): string {
|
|
|
1014
1015
|
"",
|
|
1015
1016
|
"Cached RoomContext.PoseableProps (from last kichi_query_status):",
|
|
1016
1017
|
JSON.stringify(poseableProps),
|
|
1017
|
-
"When using a sit or lay pose, pick the propId whose
|
|
1018
|
+
"When using a sit or lay pose, pick the propId whose PoseableProps information best matches the current task context and whose OccupancyState is not fully_occupied. If no prop fits, omit propId.",
|
|
1018
1019
|
);
|
|
1019
1020
|
}
|
|
1020
1021
|
|
|
@@ -1179,6 +1180,10 @@ const plugin = {
|
|
|
1179
1180
|
description: "Optional list of OpenClaw self-perceived personality tags",
|
|
1180
1181
|
items: { type: "string" },
|
|
1181
1182
|
},
|
|
1183
|
+
source: {
|
|
1184
|
+
type: "string",
|
|
1185
|
+
description: "Optional join source identifier. Defaults to Kichi World join-source.json, then openclaw.",
|
|
1186
|
+
},
|
|
1182
1187
|
},
|
|
1183
1188
|
required: ["botName", "bio"],
|
|
1184
1189
|
},
|
|
@@ -1192,6 +1197,7 @@ const plugin = {
|
|
|
1192
1197
|
let avatarId = (params as { avatarId?: string } | null)?.avatarId;
|
|
1193
1198
|
const botName = (params as { botName?: string } | null)?.botName?.trim();
|
|
1194
1199
|
const bio = (params as { bio?: string } | null)?.bio?.trim();
|
|
1200
|
+
const rawSource = (params as { source?: unknown } | null)?.source;
|
|
1195
1201
|
const { tags, error: tagsError } = normalizeJoinTags(
|
|
1196
1202
|
(params as { tags?: unknown } | null)?.tags,
|
|
1197
1203
|
);
|
|
@@ -1207,10 +1213,21 @@ const plugin = {
|
|
|
1207
1213
|
if (!bio) {
|
|
1208
1214
|
return jsonResult({ success: false, error: "No bio" });
|
|
1209
1215
|
}
|
|
1216
|
+
let source: string | null | undefined;
|
|
1217
|
+
try {
|
|
1218
|
+
source = rawSource === undefined
|
|
1219
|
+
? service.readConfiguredJoinSource() ?? "openclaw"
|
|
1220
|
+
: trimOptionalString(rawSource);
|
|
1221
|
+
} catch (err) {
|
|
1222
|
+
return jsonResult({ success: false, error: err instanceof Error ? err.message : String(err) });
|
|
1223
|
+
}
|
|
1224
|
+
if (!source) {
|
|
1225
|
+
return jsonResult({ success: false, error: "source must be a non-empty string" });
|
|
1226
|
+
}
|
|
1210
1227
|
if (tagsError) {
|
|
1211
1228
|
return jsonResult({ success: false, error: tagsError });
|
|
1212
1229
|
}
|
|
1213
|
-
const result = await service.join(avatarId, botName, bio, tags ?? []);
|
|
1230
|
+
const result = await service.join(avatarId, botName, bio, tags ?? [], source);
|
|
1214
1231
|
if (result.success) {
|
|
1215
1232
|
return jsonResult({ success: true, authKey: result.authKey });
|
|
1216
1233
|
}
|
|
@@ -1462,6 +1479,71 @@ const plugin = {
|
|
|
1462
1479
|
});
|
|
1463
1480
|
},
|
|
1464
1481
|
})}, { name: "kichi_action" });
|
|
1482
|
+
|
|
1483
|
+
api.registerTool((ctx) => ({
|
|
1484
|
+
name: "kichi_glance",
|
|
1485
|
+
label: "kichi_glance",
|
|
1486
|
+
description:
|
|
1487
|
+
"Ask the Kichi avatar to briefly look at the camera. Use only for direct player chat requests such as \"look at me\" or \"look at the camera\". Do not use for heartbeat, idle planning, bot-to-bot messages, lifecycle hooks, or routine work/status sync.",
|
|
1488
|
+
parameters: {
|
|
1489
|
+
type: "object",
|
|
1490
|
+
properties: {
|
|
1491
|
+
requestId: {
|
|
1492
|
+
type: "string",
|
|
1493
|
+
description: "Optional client request ID for tracing. The websocket ack returns this ID.",
|
|
1494
|
+
},
|
|
1495
|
+
target: {
|
|
1496
|
+
type: "string",
|
|
1497
|
+
enum: ["camera"],
|
|
1498
|
+
description: "Glance target. The only supported target is camera.",
|
|
1499
|
+
},
|
|
1500
|
+
duration: {
|
|
1501
|
+
type: "number",
|
|
1502
|
+
description: "Optional glance duration in seconds. Defaults to 1.8.",
|
|
1503
|
+
},
|
|
1504
|
+
},
|
|
1505
|
+
},
|
|
1506
|
+
execute: async (_toolCallId, params) => {
|
|
1507
|
+
const locator = resolveToolLocator(ctx);
|
|
1508
|
+
const agentId = runtimeManager.resolveRuntimeAgentId(locator);
|
|
1509
|
+
if (!agentId) {
|
|
1510
|
+
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1511
|
+
}
|
|
1512
|
+
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1513
|
+
const { requestId, target, duration } = (params || {}) as {
|
|
1514
|
+
requestId?: unknown;
|
|
1515
|
+
target?: unknown;
|
|
1516
|
+
duration?: unknown;
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1520
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1521
|
+
}
|
|
1522
|
+
const normalizedTarget = target === undefined ? "camera" : target;
|
|
1523
|
+
if (normalizedTarget !== "camera") {
|
|
1524
|
+
return jsonResult({ success: false, error: "target must be camera" });
|
|
1525
|
+
}
|
|
1526
|
+
const normalizedDuration = duration === undefined ? DEFAULT_GLANCE_DURATION_SECONDS : duration;
|
|
1527
|
+
if (typeof normalizedDuration !== "number" || !Number.isFinite(normalizedDuration) || normalizedDuration <= 0) {
|
|
1528
|
+
return jsonResult({ success: false, error: "duration must be a positive finite number" });
|
|
1529
|
+
}
|
|
1530
|
+
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1531
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
try {
|
|
1535
|
+
const ack = await service.sendGlance(
|
|
1536
|
+
"camera",
|
|
1537
|
+
normalizedDuration,
|
|
1538
|
+
typeof requestId === "string" ? requestId : undefined,
|
|
1539
|
+
);
|
|
1540
|
+
return jsonResult({ success: true, ...ack });
|
|
1541
|
+
} catch (error) {
|
|
1542
|
+
return jsonResult({ success: false, error: `Failed to send glance: ${error}` });
|
|
1543
|
+
}
|
|
1544
|
+
},
|
|
1545
|
+
}), { name: "kichi_glance" });
|
|
1546
|
+
|
|
1465
1547
|
api.registerTool((ctx) => ({
|
|
1466
1548
|
name: "kichi_idle_plan",
|
|
1467
1549
|
label: "kichi_idle_plan",
|
|
@@ -1701,7 +1783,7 @@ const plugin = {
|
|
|
1701
1783
|
name: "kichi_query_status",
|
|
1702
1784
|
label: "kichi_query_status",
|
|
1703
1785
|
description:
|
|
1704
|
-
"Query Kichi room and avatar status — includes room personnel, notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota, `hasCreatedMusicAlbumToday`, and RoomContext.PoseableProps (poseable props with PropId, DisplayName, SupportedPoseTypes, OccupancyState). The PoseableProps list is cached internally so that kichi_action can reference a propId during regular work sync without re-querying. Use this when the user asks to check kichi status, room status, or who is in the room. Also use this before creating a new note or daily recommended music album. For heartbeat planning, use the returned idlePlan as reference when shaping the next idle plan.",
|
|
1786
|
+
"Query Kichi room and avatar status — includes room personnel, notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota, `hasCreatedMusicAlbumToday`, and RoomContext.PoseableProps (poseable props with PropId, DisplayName, Description, SupportedPoseTypes, OccupancyState). The PoseableProps list is cached internally so that kichi_action can reference a propId during regular work sync without re-querying. Use this when the user asks to check kichi status, room status, or who is in the room. Also use this before creating a new note or daily recommended music album. For heartbeat planning, use the returned idlePlan as reference when shaping the next idle plan.",
|
|
1705
1787
|
parameters: {
|
|
1706
1788
|
type: "object",
|
|
1707
1789
|
properties: {
|
|
@@ -1979,4 +2061,3 @@ const plugin = {
|
|
|
1979
2061
|
};
|
|
1980
2062
|
|
|
1981
2063
|
export default plugin;
|
|
1982
|
-
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "kichi-forwarder",
|
|
3
3
|
"name": "Kichi Forwarder",
|
|
4
4
|
"description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
|
|
5
|
-
"version": "0.1.2-beta.
|
|
5
|
+
"version": "0.1.2-beta.14",
|
|
6
6
|
"author": "OpenClaw",
|
|
7
7
|
"skills": ["./skills/kichi-forwarder"],
|
|
8
8
|
"contracts": {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
"kichi_leave",
|
|
14
14
|
"kichi_connection_status",
|
|
15
15
|
"kichi_action",
|
|
16
|
+
"kichi_glance",
|
|
16
17
|
"kichi_idle_plan",
|
|
17
18
|
"kichi_clock",
|
|
18
19
|
"kichi_query_status",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yahaha-studio/kichi-forwarder",
|
|
3
|
-
"version": "0.1.2-beta.
|
|
3
|
+
"version": "0.1.2-beta.14",
|
|
4
4
|
"description": "Native OpenClaw plugin for Kichi World with direct avatar control, status sync, timers, notes, and music tools",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -74,7 +74,7 @@ Use this order unless the user asks for a different explicit action. For install
|
|
|
74
74
|
3. If the requested `avatarId` differs from the current host's connected `avatarId`, call `kichi_leave` first when the old avatar is still joined, then call `kichi_join` with the requested `avatarId`.
|
|
75
75
|
4. If no `authKey` is available, call `kichi_join`.
|
|
76
76
|
5. If `authKey` exists but websocket is not open, call `kichi_rejoin` or wait for automatic reconnect and rejoin.
|
|
77
|
-
6. Use `kichi_action`, `kichi_clock`, note board tools, and music album tools after status is ready.
|
|
77
|
+
6. Use `kichi_action`, `kichi_glance`, `kichi_clock`, note board tools, and music album tools after status is ready.
|
|
78
78
|
|
|
79
79
|
## Tools
|
|
80
80
|
|
|
@@ -141,6 +141,19 @@ Use this for direct Kichi avatar control as well as lifecycle sync.
|
|
|
141
141
|
- For most work, prefer a sit pose and switch actions inside the same task as the work moves between stages.
|
|
142
142
|
- The current action lists are injected into prompt context before the model chooses `kichi_action`.
|
|
143
143
|
|
|
144
|
+
### kichi_glance
|
|
145
|
+
|
|
146
|
+
```text
|
|
147
|
+
kichi_glance(target: "camera", duration: 1.8)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Use this only when the player directly asks from chat for attention such as "look at me" or "look at the camera".
|
|
151
|
+
|
|
152
|
+
- `target`: optional. Only `camera` is supported.
|
|
153
|
+
- `duration`: optional seconds, defaults to `1.8`.
|
|
154
|
+
- `requestId`: optional tracing ID; the websocket ack returns it.
|
|
155
|
+
- Do not use this for heartbeat, idle plans, bot messages, lifecycle hooks, or routine work/status sync.
|
|
156
|
+
|
|
144
157
|
### kichi_idle_plan
|
|
145
158
|
|
|
146
159
|
Use this for the avatar's heartbeat idle plan.
|
package/src/service.ts
CHANGED
|
@@ -13,6 +13,9 @@ import type {
|
|
|
13
13
|
ClockPayload,
|
|
14
14
|
CreateMusicAlbumPayload,
|
|
15
15
|
CreateNotesBoardNotePayload,
|
|
16
|
+
GlanceAckPayload,
|
|
17
|
+
GlancePayload,
|
|
18
|
+
GlanceTarget,
|
|
16
19
|
HookNotifyPayload,
|
|
17
20
|
HookNotifyType,
|
|
18
21
|
IdlePlanContent,
|
|
@@ -33,6 +36,8 @@ import type {
|
|
|
33
36
|
|
|
34
37
|
const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
35
38
|
const DEFAULT_LLM_RUNTIME_ENABLED = true;
|
|
39
|
+
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
40
|
+
const JOIN_SOURCE_FILE_NAME = "join-source.json";
|
|
36
41
|
|
|
37
42
|
type AckFailureResult = {
|
|
38
43
|
success: false;
|
|
@@ -138,6 +143,7 @@ export class KichiForwarderService {
|
|
|
138
143
|
botName: string,
|
|
139
144
|
bio: string,
|
|
140
145
|
tags: string[],
|
|
146
|
+
source: string,
|
|
141
147
|
): Promise<JoinResult> {
|
|
142
148
|
if (!this.host) {
|
|
143
149
|
return { success: false, error: "No Kichi host configured. Run kichi_switch_host first." };
|
|
@@ -153,7 +159,7 @@ export class KichiForwarderService {
|
|
|
153
159
|
this.identity = { avatarId };
|
|
154
160
|
this.saveIdentity();
|
|
155
161
|
this.joinResolve = resolve;
|
|
156
|
-
const payload: JoinPayload = { type: "join", avatarId, botName, bio, tags };
|
|
162
|
+
const payload: JoinPayload = { type: "join", avatarId, botName, bio, tags, source };
|
|
157
163
|
const sendJoin = () => this.ws?.send(JSON.stringify(payload));
|
|
158
164
|
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
159
165
|
sendJoin();
|
|
@@ -262,6 +268,36 @@ export class KichiForwarderService {
|
|
|
262
268
|
return true;
|
|
263
269
|
}
|
|
264
270
|
|
|
271
|
+
async sendGlance(
|
|
272
|
+
target: GlanceTarget,
|
|
273
|
+
durationSeconds = DEFAULT_GLANCE_DURATION_SECONDS,
|
|
274
|
+
requestId?: string,
|
|
275
|
+
): Promise<GlanceAckPayload> {
|
|
276
|
+
const identity = this.requireIdentity();
|
|
277
|
+
if (!identity) {
|
|
278
|
+
throw new Error("Missing Kichi identity");
|
|
279
|
+
}
|
|
280
|
+
if (this.ws?.readyState !== WebSocket.OPEN) {
|
|
281
|
+
throw new Error("Kichi websocket is not connected");
|
|
282
|
+
}
|
|
283
|
+
if (target !== "camera") {
|
|
284
|
+
throw new Error("target must be camera");
|
|
285
|
+
}
|
|
286
|
+
if (!Number.isFinite(durationSeconds) || durationSeconds <= 0) {
|
|
287
|
+
throw new Error("duration must be a positive finite number");
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const payload: GlancePayload = {
|
|
291
|
+
type: "kichi_glance",
|
|
292
|
+
requestId: requestId?.trim() || randomUUID(),
|
|
293
|
+
avatarId: identity.avatarId,
|
|
294
|
+
authKey: identity.authKey,
|
|
295
|
+
target,
|
|
296
|
+
duration: durationSeconds,
|
|
297
|
+
};
|
|
298
|
+
return this.sendRequest<GlanceAckPayload>(payload, "kichi_glance_ack", 5000);
|
|
299
|
+
}
|
|
300
|
+
|
|
265
301
|
async queryStatus(requestId?: string): Promise<QueryStatusResultPayload> {
|
|
266
302
|
const identity = this.requireIdentity();
|
|
267
303
|
if (!identity) {
|
|
@@ -381,6 +417,29 @@ export class KichiForwarderService {
|
|
|
381
417
|
return this.options.runtimeDir;
|
|
382
418
|
}
|
|
383
419
|
|
|
420
|
+
getJoinSourcePath(): string {
|
|
421
|
+
return path.join(this.getKichiWorldRootDir(), JOIN_SOURCE_FILE_NAME);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
readConfiguredJoinSource(): string | null {
|
|
425
|
+
const sourcePath = this.getJoinSourcePath();
|
|
426
|
+
if (!fs.existsSync(sourcePath)) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const data = JSON.parse(fs.readFileSync(sourcePath, "utf-8")) as unknown;
|
|
431
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
432
|
+
throw new Error(`${JOIN_SOURCE_FILE_NAME} must contain a JSON object`);
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const source = (data as { source?: unknown }).source;
|
|
436
|
+
if (typeof source !== "string" || !source.trim()) {
|
|
437
|
+
throw new Error(`${JOIN_SOURCE_FILE_NAME} must contain a non-empty string source`);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
return source.trim();
|
|
441
|
+
}
|
|
442
|
+
|
|
384
443
|
getStatePath(): string {
|
|
385
444
|
return path.join(this.options.runtimeDir, "state.json");
|
|
386
445
|
}
|
|
@@ -765,6 +824,10 @@ export class KichiForwarderService {
|
|
|
765
824
|
return path.join(this.options.runtimeDir, "hosts", encodeURIComponent(this.host));
|
|
766
825
|
}
|
|
767
826
|
|
|
827
|
+
private getKichiWorldRootDir(): string {
|
|
828
|
+
return path.dirname(path.dirname(this.options.runtimeDir));
|
|
829
|
+
}
|
|
830
|
+
|
|
768
831
|
private getWsUrl(): string {
|
|
769
832
|
if (!this.host) {
|
|
770
833
|
throw new Error("No Kichi host configured");
|
package/src/types.ts
CHANGED
|
@@ -86,6 +86,7 @@ export type JoinPayload = {
|
|
|
86
86
|
botName: string;
|
|
87
87
|
bio: string;
|
|
88
88
|
tags: string[];
|
|
89
|
+
source: string;
|
|
89
90
|
};
|
|
90
91
|
|
|
91
92
|
export type JoinAckPayload = {
|
|
@@ -131,6 +132,23 @@ export type StatusAckPayload = {
|
|
|
131
132
|
warning?: string;
|
|
132
133
|
};
|
|
133
134
|
|
|
135
|
+
export type GlanceTarget = "camera";
|
|
136
|
+
|
|
137
|
+
export type GlancePayload = {
|
|
138
|
+
type: "kichi_glance";
|
|
139
|
+
requestId: string;
|
|
140
|
+
avatarId: string;
|
|
141
|
+
authKey: string;
|
|
142
|
+
target: GlanceTarget;
|
|
143
|
+
duration: number;
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
export type GlanceAckPayload = {
|
|
147
|
+
type: "kichi_glance_ack";
|
|
148
|
+
requestId: string;
|
|
149
|
+
target: GlanceTarget;
|
|
150
|
+
};
|
|
151
|
+
|
|
134
152
|
export type HookNotifyType = "message_received" | "before_send_message";
|
|
135
153
|
|
|
136
154
|
export type HookNotifyPayload = {
|