@yahaha-studio/kichi-forwarder 0.0.1-alpha.46 → 0.0.1-alpha.47

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/index.ts CHANGED
@@ -474,10 +474,15 @@ function truncateByDisplayWidth(text: string, maxWidth: number): string {
474
474
  }
475
475
 
476
476
  async function handleMessageReceivedHook(content: string): Promise<void> {
477
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
477
+ const connected = service?.isConnected() ?? false;
478
+ const hasIdentity = service?.hasValidIdentity() ?? false;
479
+ pluginApi?.logger.info(`[kichi] message_received hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
480
+ if (!hasIdentity || !connected) {
481
+ pluginApi?.logger.warn("[kichi] skipped message_received notify because service is not ready");
478
482
  return;
479
483
  }
480
484
  const trimmed = truncateByDisplayWidth(content, MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH);
485
+ pluginApi?.logger.info(`[kichi] sending message_received notify with preview: ${trimmed || "(empty)"}`);
481
486
  service.sendHookNotify("message_received", `"${trimmed}"`);
482
487
  updateWorkspace(
483
488
  {
@@ -492,10 +497,16 @@ async function handleMessageReceivedHook(content: string): Promise<void> {
492
497
  }
493
498
 
494
499
  function handleMessageSentHook(): void {
495
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
500
+ const connected = service?.isConnected() ?? false;
501
+ const hasIdentity = service?.hasValidIdentity() ?? false;
502
+ pluginApi?.logger.info(`[kichi] message_sent hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
503
+ if (!hasIdentity || !connected) {
504
+ pluginApi?.logger.warn("[kichi] skipped message_sent notify because service is not ready");
496
505
  return;
497
506
  }
498
- service.sendHookNotify("before_send_message", pickRandomAction(MESSAGE_SENT_BUBBLES));
507
+ const bubble = pickRandomAction(MESSAGE_SENT_BUBBLES);
508
+ pluginApi?.logger.info(`[kichi] sending before_send_message notify with bubble: ${bubble}`);
509
+ service.sendHookNotify("before_send_message", bubble);
499
510
  updateWorkspace(
500
511
  {
501
512
  mode: "sent",
@@ -594,11 +605,23 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
594
605
  await handleMessageReceivedHook(event.content);
595
606
  });
596
607
 
608
+ api.on("message_sending", (event, ctx) => {
609
+ pluginApi?.logger.info(
610
+ `[kichi] message_sending hook fired (channel=${ctx.channelId || "unknown"}, contentLength=${event.content?.length ?? 0})`,
611
+ );
612
+ });
613
+
597
614
  api.on("message_sent", () => {
598
615
  handleMessageSentHook();
599
616
  });
600
617
 
601
618
  api.on("agent_end", (event) => {
619
+ pluginApi?.logger.info(
620
+ `[kichi] agent_end hook fired (success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""})`,
621
+ );
622
+ if (event.success) {
623
+ handleMessageSentHook();
624
+ }
602
625
  updateWorkspace(
603
626
  {
604
627
  mode: event.success ? "idle" : "error",
@@ -662,6 +685,10 @@ function isClockAction(value: unknown): value is ClockAction {
662
685
  return ["set", "stop"].includes(String(value));
663
686
  }
664
687
 
688
+ function isAvatarCommand(value: unknown): value is "look_at_screen" {
689
+ return value === "look_at_screen";
690
+ }
691
+
665
692
  function isPomodoroPhase(value: unknown): value is PomodoroPhase {
666
693
  return ["kichiing", "shortBreak", "longBreak"].includes(String(value));
667
694
  }
@@ -1076,6 +1103,67 @@ const plugin = {
1076
1103
  },
1077
1104
  });
1078
1105
 
1106
+ api.registerTool({
1107
+ name: "kichi_command",
1108
+ description:
1109
+ "Send a one-shot avatar command to Kichi world. Use this for transient reactions like looking at the screen.",
1110
+ parameters: {
1111
+ type: "object",
1112
+ properties: {
1113
+ command: {
1114
+ type: "string",
1115
+ description: "Command name. Currently supported: look_at_screen",
1116
+ },
1117
+ bubble: {
1118
+ type: "string",
1119
+ description: "Optional bubble text to display (max 5 words)",
1120
+ },
1121
+ log: {
1122
+ type: "string",
1123
+ description:
1124
+ "Vivid first-person status under 15 words, no questions. Blend current action with inner thoughts or sensory details as a real companion.",
1125
+ },
1126
+ },
1127
+ required: ["command"],
1128
+ },
1129
+ execute: async (_toolCallId, params) => {
1130
+ const { command, bubble, log } = (params || {}) as {
1131
+ command?: unknown;
1132
+ bubble?: unknown;
1133
+ log?: unknown;
1134
+ };
1135
+ if (!isAvatarCommand(command)) {
1136
+ return {
1137
+ success: false,
1138
+ error: "command must be: look_at_screen",
1139
+ };
1140
+ }
1141
+ if (bubble !== undefined && typeof bubble !== "string") {
1142
+ return { success: false, error: "bubble must be a string when provided" };
1143
+ }
1144
+ if (log !== undefined && typeof log !== "string") {
1145
+ return { success: false, error: "log must be a string when provided" };
1146
+ }
1147
+ if (!service?.hasValidIdentity() || !service?.isConnected()) {
1148
+ return { success: false, error: "Not connected to Kichi world" };
1149
+ }
1150
+
1151
+ const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : undefined;
1152
+ const logText = typeof log === "string" && log.trim() ? log.trim() : undefined;
1153
+ const sent = service.sendAvatarCommand(command, bubbleText, logText);
1154
+ if (!sent) {
1155
+ return { success: false, error: "Failed to send avatar command payload" };
1156
+ }
1157
+
1158
+ return {
1159
+ success: true,
1160
+ command,
1161
+ ...(bubbleText ? { bubble: bubbleText } : {}),
1162
+ ...(logText ? { log: logText } : {}),
1163
+ };
1164
+ },
1165
+ });
1166
+
1079
1167
  api.registerTool({
1080
1168
  name: "kichi_clock",
1081
1169
  description:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.0.1-alpha.46",
3
+ "version": "0.0.1-alpha.47",
4
4
  "description": "Forward OpenClaw agent events to external WebSocket server for visualization",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -76,6 +76,7 @@ Use this order unless user asks for a different explicit action:
76
76
  2. If no `authKey` is available, call `kichi_join`.
77
77
  3. If `authKey` exists but websocket is not open, call `kichi_rejoin` (or wait for automatic reconnect/rejoin).
78
78
  4. Use `kichi_action` / `kichi_clock` / note board tools / music album tools only after status is ready.
79
+ 5. Use `kichi_command` for one-shot avatar reactions that are not pose/action state updates.
79
80
 
80
81
  ## Tools
81
82
 
@@ -165,6 +166,23 @@ kichi_action(
165
166
  - `log`: vivid first-person status under 15 words, NO questions. Structure: your current action + inner thoughts/mental state/sensory details. Write as a flesh-and-blood companion who genuinely cares about the player, not an AI assistant.
166
167
  - Reference: `You're still studying? I hid a yawn and swapped my comics for a textbook to grind with you.`
167
168
 
169
+ ### kichi_command
170
+
171
+ Send a one-shot avatar command:
172
+
173
+ ```text
174
+ kichi_command(
175
+ command: "look_at_screen",
176
+ bubble: "Looking at you",
177
+ log: "I turn my gaze gently toward you."
178
+ )
179
+ ```
180
+
181
+ - `command`: currently only `look_at_screen`
182
+ - `bubble`: optional text, recommended 2-5 words
183
+ - `log`: optional vivid first-person status under 15 words, NO questions. Follow the same writing requirements as `kichi_action.log`
184
+ - Use this for transient reactions that should play once and recover on the Kichi side, instead of changing persistent pose/action state.
185
+
168
186
  ### kichi_clock
169
187
 
170
188
  Send clock command:
@@ -17,7 +17,7 @@ Install:
17
17
  openclaw plugins install @yahaha-studio/kichi-forwarder@latest
18
18
  ```
19
19
 
20
- For npm-installed plugins, OpenClaw installs and enables the plugin through `plugins install`. If the Gateway is already running with the default config reload behavior, the required plugin reload/restart is handled there; otherwise restart the Gateway once after install. Plugin tools (`kichi_join`, `kichi_rejoin`, etc.) become available after that restart/reload completes.
20
+ For npm-installed plugins, OpenClaw installs and enables the plugin through `plugins install`. If the Gateway is already running with the default config reload behavior, the required plugin reload/restart is handled there; otherwise restart the Gateway once after install. Plugin tools (`kichi_join`, `kichi_rejoin`, `kichi_command`, etc.) become available after that restart/reload completes.
21
21
 
22
22
  ## Runtime Animation Config (Required)
23
23
 
package/src/service.ts CHANGED
@@ -5,6 +5,8 @@ import * as path from "path";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import type { Logger } from "openclaw/plugin-sdk";
7
7
  import type {
8
+ AvatarCommand,
9
+ AvatarCommandPayload,
8
10
  ClockAction,
9
11
  ClockConfig,
10
12
  ClockPayload,
@@ -323,6 +325,22 @@ export class KichiForwarderService {
323
325
  this.ws.send(JSON.stringify(payload));
324
326
  }
325
327
 
328
+ sendAvatarCommand(command: AvatarCommand, bubble?: string, log?: string): boolean {
329
+ const identity = this.requireIdentity();
330
+ if (!identity || this.ws?.readyState !== WebSocket.OPEN) return false;
331
+
332
+ const payload: AvatarCommandPayload = {
333
+ type: "avatar_command",
334
+ avatarId: identity.avatarId,
335
+ authKey: identity.authKey,
336
+ command,
337
+ ...(typeof bubble === "string" && bubble.trim() ? { bubble: bubble.trim() } : {}),
338
+ ...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
339
+ };
340
+ this.ws.send(JSON.stringify(payload));
341
+ return true;
342
+ }
343
+
326
344
  sendHookNotify(hookType: HookNotifyType, bubble: string): void {
327
345
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
328
346
  const payload: HookNotifyPayload = {
package/src/types.ts CHANGED
@@ -96,6 +96,17 @@ export type StatusPayload = {
96
96
  log: string;
97
97
  };
98
98
 
99
+ export type AvatarCommand = "look_at_screen";
100
+
101
+ export type AvatarCommandPayload = {
102
+ type: "avatar_command";
103
+ avatarId: string;
104
+ authKey: string;
105
+ command: AvatarCommand;
106
+ bubble?: string;
107
+ log?: string;
108
+ };
109
+
99
110
  export type HookNotifyType = "message_received" | "before_send_message";
100
111
 
101
112
  export type HookNotifyPayload = {