@yahaha-studio/kichi-forwarder 0.1.2-beta.13 → 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 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 DisplayName best matches the current task context and whose OccupancyState is not fully_occupied. If no prop fits, omit propId.");
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
  }
@@ -1238,6 +1239,59 @@ const plugin = {
1238
1239
  },
1239
1240
  });
1240
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" });
1241
1295
  api.registerTool((ctx) => ({
1242
1296
  name: "kichi_idle_plan",
1243
1297
  label: "kichi_idle_plan",
@@ -1466,7 +1520,7 @@ const plugin = {
1466
1520
  api.registerTool((ctx) => ({
1467
1521
  name: "kichi_query_status",
1468
1522
  label: "kichi_query_status",
1469
- 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.",
1470
1524
  parameters: {
1471
1525
  type: "object",
1472
1526
  properties: {
@@ -4,6 +4,7 @@ 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;
7
8
  const JOIN_SOURCE_FILE_NAME = "join-source.json";
8
9
  export class KichiForwarderService {
9
10
  logger;
@@ -177,6 +178,30 @@ export class KichiForwarderService {
177
178
  this.ws.send(JSON.stringify(payload));
178
179
  return true;
179
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
+ }
180
205
  async queryStatus(requestId) {
181
206
  const identity = this.requireIdentity();
182
207
  if (!identity) {
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 DisplayName best matches the current task context and whose OccupancyState is not fully_occupied. If no prop fits, omit propId.",
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
 
@@ -1478,6 +1479,71 @@ const plugin = {
1478
1479
  });
1479
1480
  },
1480
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
+
1481
1547
  api.registerTool((ctx) => ({
1482
1548
  name: "kichi_idle_plan",
1483
1549
  label: "kichi_idle_plan",
@@ -1717,7 +1783,7 @@ const plugin = {
1717
1783
  name: "kichi_query_status",
1718
1784
  label: "kichi_query_status",
1719
1785
  description:
1720
- "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.",
1721
1787
  parameters: {
1722
1788
  type: "object",
1723
1789
  properties: {
@@ -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.13",
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.13",
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,7 @@ 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;
36
40
  const JOIN_SOURCE_FILE_NAME = "join-source.json";
37
41
 
38
42
  type AckFailureResult = {
@@ -264,6 +268,36 @@ export class KichiForwarderService {
264
268
  return true;
265
269
  }
266
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
+
267
301
  async queryStatus(requestId?: string): Promise<QueryStatusResultPayload> {
268
302
  const identity = this.requireIdentity();
269
303
  if (!identity) {
package/src/types.ts CHANGED
@@ -132,6 +132,23 @@ export type StatusAckPayload = {
132
132
  warning?: string;
133
133
  };
134
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
+
135
152
  export type HookNotifyType = "message_received" | "before_send_message";
136
153
 
137
154
  export type HookNotifyPayload = {