@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 +91 -3
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +18 -0
- package/skills/kichi-forwarder/references/install.md +1 -1
- package/src/service.ts +18 -0
- package/src/types.ts +11 -0
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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 = {
|