@yahaha-studio/kichi-forwarder 0.0.1-alpha.45 → 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
@@ -106,6 +106,10 @@ type WorkspaceScreenState = {
106
106
  currentFocus: string;
107
107
  hint: string;
108
108
  prompt: string;
109
+ title: string;
110
+ shellName: string;
111
+ cwdLabel: string;
112
+ modelLabel: string;
109
113
  };
110
114
 
111
115
  function createWorkspaceScreenState(): WorkspaceScreenState {
@@ -118,7 +122,43 @@ function createWorkspaceScreenState(): WorkspaceScreenState {
118
122
  recentActivity: [],
119
123
  currentFocus: "Waiting for the next thread to pick up.",
120
124
  hint: "Low-noise live workspace view.",
121
- prompt: "kiro@room:~$ _",
125
+ prompt: "$ _",
126
+ title: "Workspace",
127
+ shellName: "agent",
128
+ cwdLabel: process.cwd(),
129
+ modelLabel: "model: unknown",
130
+ };
131
+ }
132
+
133
+ function normalizeShellToken(value: string): string {
134
+ const normalized = value
135
+ .trim()
136
+ .toLowerCase()
137
+ .replace(/[^a-z0-9._-]+/g, "-")
138
+ .replace(/^-+|-+$/g, "");
139
+ return normalized || "agent";
140
+ }
141
+
142
+ function parseIdentityNameFromWorkspace(workspaceRoot: string): string | null {
143
+ try {
144
+ const identityPath = path.join(workspaceRoot, "IDENTITY.md");
145
+ if (!fs.existsSync(identityPath)) return null;
146
+ const raw = fs.readFileSync(identityPath, "utf-8");
147
+ const match = raw.match(/^-\s*\*\*Name:\*\*\s*(.+)$/m);
148
+ const value = match?.[1]?.trim();
149
+ return value || null;
150
+ } catch {
151
+ return null;
152
+ }
153
+ }
154
+
155
+ function deriveWorkspaceIdentity(workspaceRoot: string): Pick<WorkspaceScreenState, "title" | "shellName" | "cwdLabel"> {
156
+ const identityName = parseIdentityNameFromWorkspace(workspaceRoot);
157
+ const titleBase = identityName || path.basename(workspaceRoot) || "workspace";
158
+ return {
159
+ title: `${titleBase} Workspace`,
160
+ shellName: normalizeShellToken(identityName || titleBase),
161
+ cwdLabel: workspaceRoot,
122
162
  };
123
163
  }
124
164
 
@@ -161,7 +201,7 @@ function pushActivity(line: string): void {
161
201
 
162
202
  function renderWorkspaceScreen(): string {
163
203
  const innerWidth = WORKSPACE_SCREEN_WIDTH - 2;
164
- const topTitle = `${color(" Kiro Workspace ", "bold")}${color("live session", "gray")}`;
204
+ const topTitle = `${color(` ${workspaceState.title} `, "bold")}${color("live session", "gray")}`;
165
205
  const topLine = `╭─── ${topTitle}${"─".repeat(Math.max(0, innerWidth - 4 - visibleLength(topTitle)))}╮`;
166
206
  const bottomLine = `╰${"─".repeat(innerWidth)}╯`;
167
207
 
@@ -191,8 +231,8 @@ function renderWorkspaceScreen(): string {
191
231
  pushRow(`${color(" Hint", "cyan")}`);
192
232
  pushRow(` ${truncatePlain(workspaceState.hint, innerWidth - 2)}`);
193
233
  pushRow("");
194
- pushRow(` ${color("openai/gpt-5.4", "magenta")}`);
195
- pushRow(` ${color("/Users/xiaoxinshi/.openclaw/workspace", "blue")}`);
234
+ pushRow(` ${color(workspaceState.modelLabel, "magenta")}`);
235
+ pushRow(` ${color(workspaceState.cwdLabel, "blue")}`);
196
236
  pushRow(` ${color(`updated: ${workspaceState.updatedAtLabel}`, "gray")}`);
197
237
  pushRow("");
198
238
 
@@ -202,7 +242,7 @@ function renderWorkspaceScreen(): string {
202
242
  bottomLine,
203
243
  "",
204
244
  color("─".repeat(WORKSPACE_SCREEN_WIDTH), "gray"),
205
- `${color("kiro@room", "green")}:${color("~", "blue")}$ ${workspaceState.prompt.replace(/^kiro@room:~\$\s*/, "")}`,
245
+ `${color(workspaceState.shellName, "green")}:${color("~", "blue")} ${workspaceState.prompt}`,
206
246
  ].join("\n");
207
247
  }
208
248
 
@@ -434,10 +474,15 @@ function truncateByDisplayWidth(text: string, maxWidth: number): string {
434
474
  }
435
475
 
436
476
  async function handleMessageReceivedHook(content: string): Promise<void> {
437
- 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");
438
482
  return;
439
483
  }
440
484
  const trimmed = truncateByDisplayWidth(content, MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH);
485
+ pluginApi?.logger.info(`[kichi] sending message_received notify with preview: ${trimmed || "(empty)"}`);
441
486
  service.sendHookNotify("message_received", `"${trimmed}"`);
442
487
  updateWorkspace(
443
488
  {
@@ -445,24 +490,30 @@ async function handleMessageReceivedHook(content: string): Promise<void> {
445
490
  phase: "processing inbound message",
446
491
  currentFocus: trimmed ? `User message: ${trimmed}` : "Reading the latest message.",
447
492
  hint: "Inbound message updated the live workspace.",
448
- prompt: "kiro@room:~$ reading-message",
493
+ prompt: "$ reading-message",
449
494
  },
450
495
  `received message: ${trimmed || "(empty)"}`,
451
496
  );
452
497
  }
453
498
 
454
499
  function handleMessageSentHook(): void {
455
- 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");
456
505
  return;
457
506
  }
458
- 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);
459
510
  updateWorkspace(
460
511
  {
461
512
  mode: "sent",
462
513
  phase: "message delivered",
463
514
  currentFocus: "Latest reply has been sent to the active chat.",
464
515
  hint: "Workspace settles after delivery.",
465
- prompt: "kiro@room:~$ idle",
516
+ prompt: "$ idle",
466
517
  },
467
518
  "sent assistant reply",
468
519
  );
@@ -476,7 +527,7 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
476
527
  phase: "building prompt",
477
528
  currentFocus: "Preparing the next response from current session context.",
478
529
  hint: "Prompt assembly is in progress.",
479
- prompt: "kiro@room:~$ build-prompt",
530
+ prompt: "$ build-prompt",
480
531
  },
481
532
  "building prompt context",
482
533
  );
@@ -499,7 +550,8 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
499
550
  phase: `llm input · ${event.model}`,
500
551
  currentFocus: "Feeding the model the current thread and constraints.",
501
552
  hint: `provider: ${event.provider} · images: ${event.imagesCount}`,
502
- prompt: "kiro@room:~$ llm-input",
553
+ prompt: "$ llm-input",
554
+ modelLabel: `${event.provider}/${event.model}`,
503
555
  },
504
556
  `entered llm input: ${event.model}`,
505
557
  );
@@ -512,7 +564,8 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
512
564
  phase: `llm output · ${event.model}`,
513
565
  currentFocus: "Shaping model output into the visible reply.",
514
566
  hint: `assistant chunks: ${event.assistantTexts.length}`,
515
- prompt: "kiro@room:~$ draft-reply",
567
+ prompt: "$ draft-reply",
568
+ modelLabel: `${event.provider}/${event.model}`,
516
569
  },
517
570
  `received llm output: ${event.model}`,
518
571
  );
@@ -525,7 +578,7 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
525
578
  phase: `running ${event.toolName}`,
526
579
  currentFocus: `Tool call in flight: ${event.toolName}`,
527
580
  hint: `tool context: ${ctx.toolName}`,
528
- prompt: `kiro@room:~$ tool ${event.toolName}`,
581
+ prompt: `$ tool ${event.toolName}`,
529
582
  },
530
583
  `tool start: ${event.toolName}`,
531
584
  );
@@ -541,7 +594,7 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
541
594
  phase: `tool finished ${event.toolName}`,
542
595
  currentFocus: `Tool result returned from ${event.toolName}.`,
543
596
  hint: event.error ? `tool error: ${event.error}` : `tool completed in ${event.durationMs ?? 0}ms`,
544
- prompt: `kiro@room:~$ continue ${event.toolName}`,
597
+ prompt: `$ continue ${event.toolName}`,
545
598
  },
546
599
  event.error ? `tool error: ${event.toolName}` : `tool done: ${event.toolName}`,
547
600
  );
@@ -552,18 +605,30 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
552
605
  await handleMessageReceivedHook(event.content);
553
606
  });
554
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
+
555
614
  api.on("message_sent", () => {
556
615
  handleMessageSentHook();
557
616
  });
558
617
 
559
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
+ }
560
625
  updateWorkspace(
561
626
  {
562
627
  mode: event.success ? "idle" : "error",
563
628
  phase: event.success ? "run complete" : "run failed",
564
629
  currentFocus: event.success ? "Run complete. Waiting for the next thread." : `Run failed: ${event.error ?? "unknown error"}`,
565
630
  hint: `duration: ${event.durationMs ?? 0}ms`,
566
- prompt: event.success ? "kiro@room:~$ _" : "kiro@room:~$ recover",
631
+ prompt: event.success ? "$ _" : "$ recover",
567
632
  },
568
633
  event.success ? "agent run complete" : "agent run failed",
569
634
  );
@@ -620,6 +685,10 @@ function isClockAction(value: unknown): value is ClockAction {
620
685
  return ["set", "stop"].includes(String(value));
621
686
  }
622
687
 
688
+ function isAvatarCommand(value: unknown): value is "look_at_screen" {
689
+ return value === "look_at_screen";
690
+ }
691
+
623
692
  function isPomodoroPhase(value: unknown): value is PomodoroPhase {
624
693
  return ["kichiing", "shortBreak", "longBreak"].includes(String(value));
625
694
  }
@@ -828,7 +897,11 @@ const plugin = {
828
897
  ctx.config.plugins?.entries?.["kichi-forwarder"]?.config,
829
898
  ) as KichiForwarderConfig;
830
899
  service = new KichiForwarderService(cfg, api.logger);
831
- workspaceState = createWorkspaceScreenState();
900
+ const workspaceRoot = ctx.repoPath ?? "/Users/xiaoxinshi/.openclaw/workspace";
901
+ workspaceState = {
902
+ ...createWorkspaceScreenState(),
903
+ ...deriveWorkspaceIdentity(workspaceRoot),
904
+ };
832
905
  workspaceState.channel = ctx.channelId ?? "unknown";
833
906
  scheduleWorkspacePush();
834
907
  return service.start();
@@ -1030,6 +1103,67 @@ const plugin = {
1030
1103
  },
1031
1104
  });
1032
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
+
1033
1167
  api.registerTool({
1034
1168
  name: "kichi_clock",
1035
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.45",
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 = {