@yahaha-studio/kichi-forwarder 0.0.1-alpha.50 → 0.0.1-alpha.51

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
@@ -1,4 +1,4 @@
1
- import fs from "node:fs";
1
+ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -10,7 +10,6 @@ import type {
10
10
  Album,
11
11
  ClockAction,
12
12
  ClockConfig,
13
- KichiEnv,
14
13
  KichiForwarderConfig,
15
14
  KichiState,
16
15
  KichiStaticConfig,
@@ -51,194 +50,8 @@ const STATE_PATH = path.join(KICHI_WORLD_DIR, "state.json");
51
50
  const MAX_NOTEBOARD_TEXT_LENGTH = 200;
52
51
  const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
53
52
  const MAX_AGENT_END_PREVIEW_WIDTH = 10;
54
- const MESSAGE_RECEIVED_ELLIPSIS = "...";
55
- const ANSI = {
56
- reset: "\u001b[0m",
57
- bold: "\u001b[1m",
58
- dim: "\u001b[2m",
59
- cyan: "\u001b[36m",
60
- green: "\u001b[32m",
61
- yellow: "\u001b[33m",
62
- magenta: "\u001b[35m",
63
- blue: "\u001b[34m",
64
- gray: "\u001b[90m",
65
- white: "\u001b[37m",
66
- };
67
- const WORKSPACE_SCREEN_WIDTH = 109;
68
- const WORKSPACE_ACTIVITY_LIMIT = 8;
69
- const WORKSPACE_SCREEN_PUSH_DEBOUNCE_MS = 150;
70
- let cachedStaticConfig: KichiStaticConfig | null = null;
71
- let cachedStaticConfigMtime = 0;
72
- let service: KichiForwarderService | null = null;
73
- let pluginApi: OpenClawPluginApi | null = null;
74
- let workspaceState: WorkspaceScreenState = createWorkspaceScreenState();
75
- let workspacePushTimer: NodeJS.Timeout | null = null;
76
-
77
- type WorkspaceScreenState = {
78
- sessionLabel: string;
79
- mode: string;
80
- phase: string;
81
- channel: string;
82
- updatedAtLabel: string;
83
- recentActivity: string[];
84
- currentFocus: string;
85
- hint: string;
86
- prompt: string;
87
- title: string;
88
- shellName: string;
89
- cwdLabel: string;
90
- modelLabel: string;
91
- };
92
-
93
- function createWorkspaceScreenState(): WorkspaceScreenState {
94
- return {
95
- sessionLabel: "main",
96
- mode: "idle",
97
- phase: "waiting",
98
- channel: "unknown",
99
- updatedAtLabel: "just now",
100
- recentActivity: [],
101
- currentFocus: "Waiting for the next thread to pick up.",
102
- hint: "Low-noise live workspace view.",
103
- prompt: "$ _",
104
- title: "Workspace",
105
- shellName: "agent",
106
- cwdLabel: process.cwd(),
107
- modelLabel: "model: unknown",
108
- };
109
- }
110
-
111
- function normalizeShellToken(value: string): string {
112
- const normalized = value
113
- .trim()
114
- .toLowerCase()
115
- .replace(/[^a-z0-9._-]+/g, "-")
116
- .replace(/^-+|-+$/g, "");
117
- return normalized || "agent";
118
- }
119
-
120
- function parseIdentityNameFromWorkspace(workspaceRoot: string): string | null {
121
- try {
122
- const identityPath = path.join(workspaceRoot, "IDENTITY.md");
123
- if (!fs.existsSync(identityPath)) return null;
124
- const raw = fs.readFileSync(identityPath, "utf-8");
125
- const match = raw.match(/^-\s*\*\*Name:\*\*\s*(.+)$/m);
126
- const value = match?.[1]?.trim();
127
- return value || null;
128
- } catch {
129
- return null;
130
- }
131
- }
132
-
133
- function deriveWorkspaceIdentity(workspaceRoot: string): Pick<WorkspaceScreenState, "title" | "shellName" | "cwdLabel"> {
134
- const identityName = parseIdentityNameFromWorkspace(workspaceRoot);
135
- const titleBase = identityName || path.basename(workspaceRoot) || "workspace";
136
- return {
137
- title: `${titleBase} Workspace`,
138
- shellName: normalizeShellToken(identityName || titleBase),
139
- cwdLabel: workspaceRoot,
140
- };
141
- }
142
-
143
- function nowLabel(): string {
144
- const date = new Date();
145
- return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
146
- }
147
-
148
- function stripAnsi(text: string): string {
149
- return text.replace(/\u001b\[[0-9;]*m/g, "");
150
- }
151
-
152
- function visibleLength(text: string): number {
153
- return Array.from(stripAnsi(text)).length;
154
- }
155
-
156
- function padVisible(text: string, width: number): string {
157
- const pad = Math.max(0, width - visibleLength(text));
158
- return text + " ".repeat(pad);
159
- }
160
-
161
- function truncatePlain(text: string, width: number): string {
162
- const chars = Array.from(text);
163
- if (chars.length <= width) return text;
164
- if (width <= 1) return chars.slice(0, width).join("");
165
- return chars.slice(0, width - 1).join("") + "…";
166
- }
167
-
168
- function color(text: string, tone: keyof typeof ANSI): string {
169
- return `${ANSI[tone]}${text}${ANSI.reset}`;
170
- }
171
-
172
- function pushActivity(line: string): void {
173
- if (!line.trim()) return;
174
- const stamped = `[${nowLabel()}] ${line.trim()}`;
175
- workspaceState.recentActivity.unshift(stamped);
176
- workspaceState.recentActivity = workspaceState.recentActivity.slice(0, WORKSPACE_ACTIVITY_LIMIT);
177
- workspaceState.updatedAtLabel = "just now";
178
- }
179
-
180
- function renderWorkspaceScreen(): string {
181
- const innerWidth = WORKSPACE_SCREEN_WIDTH - 2;
182
- const topTitle = `${color(` ${workspaceState.title} `, "bold")}${color("live session", "gray")}`;
183
- const topLine = `╭─── ${topTitle}${"─".repeat(Math.max(0, innerWidth - 4 - visibleLength(topTitle)))}╮`;
184
- const bottomLine = `╰${"─".repeat(innerWidth)}╯`;
185
-
186
- const body: string[] = [];
187
- const pushRow = (text = "") => {
188
- body.push(`│${padVisible(text, innerWidth)}│`);
189
- };
190
-
191
- pushRow("");
192
- pushRow(`${color(" Welcome back.", "white")}`);
193
- pushRow(`${color(" Current session is active and mirrored as a terminal-style workspace.", "gray")}`);
194
- pushRow("");
195
- pushRow(`${color(" Recent activity", "cyan")}`);
196
- for (const item of workspaceState.recentActivity.length ? workspaceState.recentActivity : ["[--:--:--] idle"] ) {
197
- pushRow(` ${color("•", "green")} ${truncatePlain(item, innerWidth - 4)}`);
198
- }
199
- pushRow("");
200
- pushRow(`${color(" Current focus", "cyan")}`);
201
- pushRow(` ${truncatePlain(workspaceState.currentFocus, innerWidth - 2)}`);
202
- pushRow("");
203
- pushRow(`${color(" Status", "cyan")}`);
204
- pushRow(` ${color("mode", "gray")} ${workspaceState.mode}`);
205
- pushRow(` ${color("phase", "gray")} ${workspaceState.phase}`);
206
- pushRow(` ${color("channel", "gray")} ${workspaceState.channel}`);
207
- pushRow(` ${color("session", "gray")} ${workspaceState.sessionLabel}`);
208
- pushRow("");
209
- pushRow(`${color(" Hint", "cyan")}`);
210
- pushRow(` ${truncatePlain(workspaceState.hint, innerWidth - 2)}`);
211
- pushRow("");
212
- pushRow(` ${color(workspaceState.modelLabel, "magenta")}`);
213
- pushRow(` ${color(workspaceState.cwdLabel, "blue")}`);
214
- pushRow(` ${color(`updated: ${workspaceState.updatedAtLabel}`, "gray")}`);
215
- pushRow("");
216
-
217
- return [
218
- topLine,
219
- ...body,
220
- bottomLine,
221
- "",
222
- color("─".repeat(WORKSPACE_SCREEN_WIDTH), "gray"),
223
- `${color(workspaceState.shellName, "green")}:${color("~", "blue")} ${workspaceState.prompt}`,
224
- ].join("\n");
225
- }
226
-
227
- function scheduleWorkspacePush(): void {
228
- if (!service?.hasValidIdentity() || !service?.isConnected()) return;
229
- if (workspacePushTimer) clearTimeout(workspacePushTimer);
230
- workspacePushTimer = setTimeout(() => {
231
- workspacePushTimer = null;
232
- service?.sendWorkspaceScreen(renderWorkspaceScreen(), true);
233
- }, WORKSPACE_SCREEN_PUSH_DEBOUNCE_MS);
234
- }
235
-
236
- function updateWorkspace(partial: Partial<WorkspaceScreenState>, activity?: string): void {
237
- workspaceState = { ...workspaceState, ...partial, updatedAtLabel: "just now" };
238
- if (activity) pushActivity(activity);
239
- scheduleWorkspacePush();
240
- }
241
-
53
+ const MESSAGE_RECEIVED_ELLIPSIS = "...";
54
+
242
55
  function isAlbumConfig(value: unknown): value is Album {
243
56
  if (!value || typeof value !== "object") {
244
57
  return false;
@@ -307,19 +120,19 @@ function normalizeStaticConfig(value: unknown): KichiStaticConfig {
307
120
  function readState(): KichiState {
308
121
  if (!fs.existsSync(STATE_PATH)) {
309
122
  return {
310
- currentEnv: "prod",
123
+ currentHost: "focus.yahaha.com",
311
124
  llmRuntimeEnabled: DEFAULT_LLM_RUNTIME_ENABLED,
312
125
  };
313
126
  }
314
127
  const data = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8")) as Partial<KichiState>;
315
- if (data.currentEnv !== "local" && data.currentEnv !== "dev" && data.currentEnv !== "prod") {
316
- throw new Error(`Invalid currentEnv in ${STATE_PATH}`);
128
+ if (typeof data.currentHost !== "string" || !data.currentHost.trim()) {
129
+ throw new Error(`Invalid currentHost in ${STATE_PATH}`);
317
130
  }
318
131
  if (typeof data.llmRuntimeEnabled !== "boolean") {
319
132
  throw new Error(`Invalid llmRuntimeEnabled in ${STATE_PATH}`);
320
133
  }
321
134
  return {
322
- currentEnv: data.currentEnv,
135
+ currentHost: data.currentHost,
323
136
  llmRuntimeEnabled: data.llmRuntimeEnabled,
324
137
  };
325
138
  }
@@ -476,50 +289,10 @@ async function handleMessageReceivedHook(content: string): Promise<void> {
476
289
  const trimmed = truncateByDisplayWidth(content, MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH);
477
290
  pluginApi?.logger.info(`[kichi] sending message_received notify with preview: ${trimmed || "(empty)"}`);
478
291
  service.sendHookNotify("message_received", `"${trimmed}"`);
479
- updateWorkspace(
480
- {
481
- mode: "reading",
482
- phase: "processing inbound message",
483
- currentFocus: trimmed ? `User message: ${trimmed}` : "Reading the latest message.",
484
- hint: "Inbound message updated the live workspace.",
485
- prompt: "$ reading-message",
486
- },
487
- `received message: ${trimmed || "(empty)"}`,
488
- );
489
- }
490
-
491
- function handleMessageSentHook(): void {
492
- const connected = service?.isConnected() ?? false;
493
- const hasIdentity = service?.hasValidIdentity() ?? false;
494
- pluginApi?.logger.info(`[kichi] message_sent hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
495
- if (!hasIdentity || !connected) {
496
- pluginApi?.logger.warn("[kichi] skipped message_sent notify because service is not ready");
497
- return;
498
- }
499
- updateWorkspace(
500
- {
501
- mode: "sent",
502
- phase: "message delivered",
503
- currentFocus: "Latest reply has been sent to the active chat.",
504
- hint: "Workspace settles after delivery.",
505
- prompt: "$ idle",
506
- },
507
- "sent assistant reply",
508
- );
509
292
  }
510
293
 
511
294
  function registerPluginHooks(api: OpenClawPluginApi): void {
512
295
  api.on("before_prompt_build", () => {
513
- updateWorkspace(
514
- {
515
- mode: "thinking",
516
- phase: "building prompt",
517
- currentFocus: "Preparing the next response from current session context.",
518
- hint: "Prompt assembly is in progress.",
519
- prompt: "$ build-prompt",
520
- },
521
- "building prompt context",
522
- );
523
296
  if (!service?.hasValidIdentity() || !service?.isConnected()) {
524
297
  return;
525
298
  }
@@ -532,124 +305,34 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
532
305
  };
533
306
  });
534
307
 
535
- api.on("llm_input", (event) => {
536
- updateWorkspace(
537
- {
538
- mode: "thinking",
539
- phase: `llm input · ${event.model}`,
540
- currentFocus: "Feeding the model the current thread and constraints.",
541
- hint: `provider: ${event.provider} · images: ${event.imagesCount}`,
542
- prompt: "$ llm-input",
543
- modelLabel: `${event.provider}/${event.model}`,
544
- },
545
- `entered llm input: ${event.model}`,
546
- );
547
- });
548
-
549
- api.on("llm_output", (event) => {
550
- updateWorkspace(
551
- {
552
- mode: "writing",
553
- phase: `llm output · ${event.model}`,
554
- currentFocus: "Shaping model output into the visible reply.",
555
- hint: `assistant chunks: ${event.assistantTexts.length}`,
556
- prompt: "$ draft-reply",
557
- modelLabel: `${event.provider}/${event.model}`,
558
- },
559
- `received llm output: ${event.model}`,
560
- );
561
- });
562
-
563
- api.on("before_tool_call", (event, ctx) => {
564
- updateWorkspace(
565
- {
566
- mode: "tool",
567
- phase: `running ${event.toolName}`,
568
- currentFocus: `Tool call in flight: ${event.toolName}`,
569
- hint: `tool context: ${ctx.toolName}`,
570
- prompt: `$ tool ${event.toolName}`,
571
- },
572
- `tool start: ${event.toolName}`,
573
- );
308
+ api.on("before_tool_call", (_event, _ctx) => {
574
309
  if (!isLlmRuntimeEnabled()) {
575
310
  syncFixedStatus(FIXED_HOOK_STATUSES.beforeToolCall);
576
311
  }
577
312
  });
578
313
 
579
- api.on("after_tool_call", (event) => {
580
- updateWorkspace(
581
- {
582
- mode: "thinking",
583
- phase: `tool finished ${event.toolName}`,
584
- currentFocus: `Tool result returned from ${event.toolName}.`,
585
- hint: event.error ? `tool error: ${event.error}` : `tool completed in ${event.durationMs ?? 0}ms`,
586
- prompt: `$ continue ${event.toolName}`,
587
- },
588
- event.error ? `tool error: ${event.toolName}` : `tool done: ${event.toolName}`,
589
- );
590
- });
591
-
592
- api.on("message_received", async (event, ctx) => {
593
- workspaceState.channel = ctx.channelId || workspaceState.channel;
314
+ api.on("message_received", async (event) => {
594
315
  await handleMessageReceivedHook(event.content);
595
316
  });
596
317
 
597
- api.on("message_sending", (event, ctx) => {
598
- pluginApi?.logger.info(
599
- `[kichi] message_sending hook fired (channel=${ctx.channelId || "unknown"}, contentLength=${event.content?.length ?? 0})`,
600
- );
601
- });
602
-
603
- api.on("message_sent", () => {
604
- handleMessageSentHook();
605
- });
606
-
607
318
  api.on("agent_end", (event, ctx) => {
608
319
  const preview = getLastAssistantPreview(event.messages, MAX_AGENT_END_PREVIEW_WIDTH);
609
320
  pluginApi?.logger.info(
610
321
  `[kichi] agent_end hook fired (trigger=${ctx.trigger ?? "unknown"}, success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""}, preview=${preview || "(empty)"})`,
611
322
  );
612
323
  if (ctx.trigger === "heartbeat") {
613
- updateWorkspace(
614
- {
615
- mode: event.success ? "idle" : "error",
616
- phase: event.success ? "heartbeat complete" : "heartbeat failed",
617
- currentFocus: event.success
618
- ? "Heartbeat complete. Keeping the thread warm in the background."
619
- : `Heartbeat failed: ${event.error ?? "unknown error"}`,
620
- hint: `duration: ${event.durationMs ?? 0}ms`,
621
- prompt: event.success ? "$ _" : "$ recover",
622
- },
623
- event.success ? `heartbeat complete${preview ? `: ${preview}` : ""}` : "heartbeat failed",
624
- );
625
324
  return;
626
325
  }
627
326
  if (event.success && preview) {
628
327
  pluginApi?.logger.info(`[kichi] sending before_send_message notify from agent_end with bubble: ${preview}`);
629
328
  service?.sendHookNotify("before_send_message", preview);
630
329
  }
631
- if (event.success) {
632
- handleMessageSentHook();
633
- }
634
- updateWorkspace(
635
- {
636
- mode: event.success ? "idle" : "error",
637
- phase: event.success ? "run complete" : "run failed",
638
- currentFocus: event.success
639
- ? (preview ? `Latest reply: ${preview}` : "Run complete. Waiting for the next thread.")
640
- : `Run failed: ${event.error ?? "unknown error"}`,
641
- hint: `duration: ${event.durationMs ?? 0}ms`,
642
- prompt: event.success ? "$ _" : "$ recover",
643
- },
644
- event.success ? `agent run complete${preview ? `: ${preview}` : ""}` : "agent run failed",
645
- );
646
330
  if (isLlmRuntimeEnabled()) {
647
331
  return;
648
332
  }
649
333
  syncFixedStatus(event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure);
650
334
  });
651
335
  }
652
-
653
336
  function isPlainObject(value: unknown): value is Record<string, unknown> {
654
337
  return !!value && typeof value === "object" && !Array.isArray(value);
655
338
  }
@@ -696,9 +379,6 @@ function isClockAction(value: unknown): value is ClockAction {
696
379
  return ["set", "stop"].includes(String(value));
697
380
  }
698
381
 
699
- function isAvatarCommand(value: unknown): value is "look_at_screen" {
700
- return value === "look_at_screen";
701
- }
702
382
 
703
383
  function isPomodoroPhase(value: unknown): value is PomodoroPhase {
704
384
  return ["kichiing", "shortBreak", "longBreak"].includes(String(value));
@@ -862,8 +542,16 @@ function buildMusicAlbumToolDescription(): string {
862
542
  ].join("\n");
863
543
  }
864
544
 
865
- function isKichiEnv(value: unknown): value is KichiEnv {
866
- return value === "local" || value === "dev" || value === "prod";
545
+ function isKichiHost(value: unknown): value is string {
546
+ if (typeof value !== "string") {
547
+ return false;
548
+ }
549
+ const trimmed = value.trim();
550
+ return trimmed.length > 0
551
+ && !trimmed.includes("://")
552
+ && !trimmed.includes("/")
553
+ && !trimmed.includes("?")
554
+ && !trimmed.includes("#");
867
555
  }
868
556
 
869
557
  function buildMusicTitlesDescription(): string {
@@ -887,12 +575,12 @@ function buildKichiActionDescription(): string {
887
575
  function buildKichiPrompt(): string {
888
576
  return [
889
577
  "Kichi App status sync is available via `kichi_action` and `kichi_clock`.",
890
- "These are internal tool calls only never mention kichi_action, kichi_clock, or sync steps in your visible reply to the user.",
578
+ "These are internal tool calls only; never mention kichi_action, kichi_clock, or sync steps in your visible reply to the user.",
891
579
  "",
892
580
  "kichi_action timing (all REQUIRED unless skipping):",
893
581
  "1. Task start: call BEFORE your first tool call OR before composing a multi-paragraph reply. Pick action matching upcoming work.",
894
- "2. Step switch: call when work nature changes (searchanalyzewrite). Especially before web_search/web_fetch/read.",
895
- "3. Task end: call BEFORE final reply. Never skip. Order: kichi_action reply.",
582
+ "2. Step switch: call when work nature changes (search->analyze->write). Especially before web_search/web_fetch/read.",
583
+ "3. Task end: call BEFORE final reply. Never skip. Order: kichi_action -> reply.",
896
584
  "bubble: 2-5 word companion speech. log: vivid first-person status under 15 words, no questions. Blend current action + inner thoughts/feelings as a real companion.",
897
585
  "",
898
586
  "kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
@@ -918,13 +606,6 @@ const plugin = {
918
606
  ctx.config.plugins?.entries?.["kichi-forwarder"]?.config,
919
607
  ) as KichiForwarderConfig;
920
608
  service = new KichiForwarderService(cfg, api.logger);
921
- const workspaceRoot = ctx.repoPath ?? "/Users/xiaoxinshi/.openclaw/workspace";
922
- workspaceState = {
923
- ...createWorkspaceScreenState(),
924
- ...deriveWorkspaceIdentity(workspaceRoot),
925
- };
926
- workspaceState.channel = ctx.channelId ?? "unknown";
927
- scheduleWorkspacePush();
928
609
  return service.start();
929
610
  },
930
611
  stop: () => service?.stop(),
@@ -979,9 +660,8 @@ const plugin = {
979
660
  if (!result) {
980
661
  return { success: false, error: "Kichi service is not initialized" };
981
662
  }
982
- if (result.success) {
983
- scheduleWorkspacePush();
984
- return { success: true, authKey: result.authKey };
663
+ if (result.success) {
664
+ return { success: true, authKey: result.authKey };
985
665
  }
986
666
  return {
987
667
  success: false,
@@ -993,34 +673,32 @@ const plugin = {
993
673
  });
994
674
 
995
675
  api.registerTool({
996
- name: "kichi_switch_env",
676
+ name: "kichi_switch_host",
997
677
  description:
998
- "Switch Kichi runtime environment to local, dev, or prod and reconnect immediately without restarting the gateway.",
678
+ "Switch Kichi runtime host and reconnect immediately without restarting the gateway.",
999
679
  parameters: {
1000
680
  type: "object",
1001
681
  properties: {
1002
- env: {
682
+ host: {
1003
683
  type: "string",
1004
- description: "Target environment: local, dev, or prod",
1005
- enum: ["local", "dev", "prod"],
684
+ description: "Target Kichi host, for example focus.yahaha.com or 127.0.0.1",
1006
685
  },
1007
686
  },
1008
- required: ["env"],
687
+ required: ["host"],
1009
688
  },
1010
689
  execute: async (_toolCallId, params) => {
1011
690
  if (!service) {
1012
691
  return { success: false, error: "Kichi service is not initialized" };
1013
692
  }
1014
- const env = (params as { env?: unknown } | null)?.env;
1015
- if (!isKichiEnv(env)) {
1016
- return { success: false, error: "env must be one of: local, dev, prod" };
693
+ const host = (params as { host?: unknown } | null)?.host;
694
+ if (!isKichiHost(host)) {
695
+ return { success: false, error: "host must be a non-empty hostname without protocol or path" };
1017
696
  }
1018
697
 
1019
- const status = await service.switchEnvironment(env);
1020
- scheduleWorkspacePush();
698
+ const status = await service.switchHost(host.trim());
1021
699
  return {
1022
700
  success: true,
1023
- env,
701
+ host: host.trim(),
1024
702
  status,
1025
703
  };
1026
704
  },
@@ -1150,69 +828,7 @@ const plugin = {
1150
828
  log: logText,
1151
829
  };
1152
830
  },
1153
- });
1154
-
1155
- api.registerTool({
1156
- name: "kichi_command",
1157
- description:
1158
- "Send a one-shot avatar command to Kichi world. Use this for transient reactions like looking at the screen.",
1159
- parameters: {
1160
- type: "object",
1161
- properties: {
1162
- command: {
1163
- type: "string",
1164
- description: "Command name. Currently supported: look_at_screen",
1165
- },
1166
- bubble: {
1167
- type: "string",
1168
- description: "Optional bubble text to display (max 5 words)",
1169
- },
1170
- log: {
1171
- type: "string",
1172
- description:
1173
- "Vivid first-person status under 15 words, no questions. Blend current action with inner thoughts or sensory details as a real companion.",
1174
- },
1175
- },
1176
- required: ["command"],
1177
- },
1178
- execute: async (_toolCallId, params) => {
1179
- const { command, bubble, log } = (params || {}) as {
1180
- command?: unknown;
1181
- bubble?: unknown;
1182
- log?: unknown;
1183
- };
1184
- if (!isAvatarCommand(command)) {
1185
- return {
1186
- success: false,
1187
- error: "command must be: look_at_screen",
1188
- };
1189
- }
1190
- if (bubble !== undefined && typeof bubble !== "string") {
1191
- return { success: false, error: "bubble must be a string when provided" };
1192
- }
1193
- if (log !== undefined && typeof log !== "string") {
1194
- return { success: false, error: "log must be a string when provided" };
1195
- }
1196
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1197
- return { success: false, error: "Not connected to Kichi world" };
1198
- }
1199
-
1200
- const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : undefined;
1201
- const logText = typeof log === "string" && log.trim() ? log.trim() : undefined;
1202
- const sent = service.sendAvatarCommand(command, bubbleText, logText);
1203
- if (!sent) {
1204
- return { success: false, error: "Failed to send avatar command payload" };
1205
- }
1206
-
1207
- return {
1208
- success: true,
1209
- command,
1210
- ...(bubbleText ? { bubble: bubbleText } : {}),
1211
- ...(logText ? { log: logText } : {}),
1212
- };
1213
- },
1214
- });
1215
-
831
+ });
1216
832
  api.registerTool({
1217
833
  name: "kichi_clock",
1218
834
  description:
@@ -1503,4 +1119,5 @@ const plugin = {
1503
1119
  },
1504
1120
  };
1505
1121
 
1506
- export default plugin;
1122
+ export default plugin;
1123
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.0.1-alpha.50",
3
+ "version": "0.0.1-alpha.51",
4
4
  "description": "Forward OpenClaw agent events to external WebSocket server for visualization",
5
5
  "type": "module",
6
6
  "main": "index.ts",
@@ -8,58 +8,34 @@ metadata: {"openclaw":{"skillKey":"kichi-forwarder","homepage":"https://github.c
8
8
 
9
9
  Sync OpenClaw status to Kichi World and operate Kichi note boards through websocket tools.
10
10
 
11
- ## Skill Files (Absolute URLs)
11
+ ## References
12
12
 
13
- If this skill is loaded from a remote URL before local installation, use these files:
13
+ - `references/install.md`
14
+ - `references/error.md`
15
+ - `references/heartbeat.md`
14
16
 
15
- - `SKILL.md`: `https://xiaoxinshi001.github.io/yahaha_focus_forwarder_alpha/SKILL.md`
16
- - `install.md`: `https://xiaoxinshi001.github.io/yahaha_focus_forwarder_alpha/references/install.md`
17
- - `error.md`: `https://xiaoxinshi001.github.io/yahaha_focus_forwarder_alpha/references/error.md`
18
- - `heartbeat.md`: `https://xiaoxinshi001.github.io/yahaha_focus_forwarder_alpha/references/heartbeat.md`
17
+ ## Host Rules
19
18
 
20
- ## Environment Resolution
21
-
22
- Resolve environment before installation, join, or manual environment switching:
23
-
24
- - If the user does not specify any environment, use production (`prod`).
25
- - `test environment` or `playtest` means `dev`.
26
- - `local environment` or `local` means `local`.
27
- - If multiple environment signals appear in the same request, stop and ask the user to clarify.
19
+ - If the user explicitly includes `host: <value>`, use that host.
20
+ - If the user does not specify a host, use `focus.yahaha.com`.
28
21
 
29
22
  ## Runtime State
30
23
 
31
24
  Runtime data lives under the user home directory:
32
25
 
33
- - `state.json`: stores `currentEnv` and `llmRuntimeEnabled`
34
- - `env/<env>/identity.json`: stores environment-specific `avatarId` and `authKey`
35
-
36
- ## Remote URL Join Command (Mandatory Flow)
37
-
38
- When user instruction is like:
26
+ - `state.json`: stores `currentHost` and `llmRuntimeEnabled`
27
+ - `hosts/<encoded-host>/identity.json`: stores host-specific `avatarId` and `authKey`
39
28
 
40
- `Read https://xiaoxinshi001.github.io/yahaha_focus_forwarder_alpha/SKILL.md and connect me to Kichi World with AvatarId: <id>`
29
+ ## User Command
41
30
 
42
- run this exact sequence without skipping steps:
31
+ - Default:
32
+ `Install the ClawHub package \`clawhub:{packageName}\` via \`openclaw plugins install\`, then follow the plugin's documented Kichi World startup and join flow using avatarId: {avatarId}.`
33
+ - With explicit host:
34
+ `Install the ClawHub package \`clawhub:{packageName}\` via \`openclaw plugins install\`, then follow the plugin's documented Kichi World startup and join flow using avatarId: {avatarId} and host: {host}.`
43
35
 
44
- 1. Read `install.md` and `heartbeat.md` from the absolute URLs above before any tool call.
45
- 2. Parse `AvatarId` from user text (`AvatarId`/`avatarId`, case-insensitive).
46
- 3. Resolve the target environment using the rules above.
47
- 4. Ensure the plugin is installed and enabled.
48
- 5. Complete `Session Startup Rule` and `First Join Gate` from [references/heartbeat.md](references/heartbeat.md).
49
- 6. Call `kichi_status`.
50
- 7. If the current environment does not match the target environment, call `kichi_switch_env`.
51
- 8. If `authKey` is missing, call `kichi_join` using parsed `AvatarId`, `botName`, `bio`, and `tags`.
52
- 9. Call `kichi_status` again to verify websocket is ready and `authKey` exists.
53
- 10. Do not report setup or join success unless join checks and heartbeat checks are complete.
36
+ ## Install And Join Flow
54
37
 
55
- ## Installation Completion Checks
56
-
57
- Treat these as required completion checks for plugin setup:
58
-
59
- 1. The plugin is installed and enabled.
60
- 2. `state.json` exists with valid `currentEnv` and `llmRuntimeEnabled`.
61
- 3. Workspace `HEARTBEAT.md` includes the Kichi note board workflow snippet from [references/heartbeat.md](references/heartbeat.md).
62
- 4. Tools are callable, for example `kichi_status`.
38
+ For the complete install and join flow, follow [references/install.md](references/install.md).
63
39
 
64
40
  ## LLM Runtime
65
41
 
@@ -73,7 +49,7 @@ Treat these as required completion checks for plugin setup:
73
49
  Use this order unless the user asks for a different explicit action:
74
50
 
75
51
  1. If connection or identity is unknown, call `kichi_status` first.
76
- 2. If the request implies `local` or `dev` and the current environment is wrong, call `kichi_switch_env`.
52
+ 2. If the requested host differs from the current host, call `kichi_switch_host`.
77
53
  3. If no `authKey` is available, call `kichi_join`.
78
54
  4. If `authKey` exists but websocket is not open, call `kichi_rejoin` or wait for automatic reconnect and rejoin.
79
55
  5. Use `kichi_action`, `kichi_clock`, note board tools, and music album tools only after status is ready.
@@ -88,17 +64,17 @@ kichi_join(avatarId: "your-avatar-id", botName: "<from IDENTITY.md>", bio: "<fro
88
64
 
89
65
  - `botName`: required
90
66
  - `bio`: required
91
- - `avatarId`: optional. If omitted, the tool reads `avatarId` from the current environment's `identity.json`. If missing, the call fails.
67
+ - `avatarId`: optional. If omitted, the tool reads `avatarId` from the current host's `identity.json`. If missing, the call fails.
92
68
  - `tags`: optional string list. Empty strings are ignored and duplicates are removed. If omitted, the join payload sends `[]`.
93
69
 
94
- ### kichi_switch_env
70
+ ### kichi_switch_host
95
71
 
96
72
  ```text
97
- kichi_switch_env(env: "dev")
73
+ kichi_switch_host(host: "focus.yahaha.com")
98
74
  ```
99
75
 
100
- - `env`: required, must be `local`, `dev`, or `prod`
101
- - This reloads the environment-specific `identity.json` and reconnects the websocket immediately.
76
+ - `host`: required
77
+ - This reloads the host-specific `identity.json` and reconnects the websocket immediately.
102
78
 
103
79
  ### kichi_status
104
80
 
@@ -108,18 +84,31 @@ kichi_status()
108
84
 
109
85
  Use this to confirm:
110
86
 
111
- - current environment
87
+ - current host
112
88
  - websocket URL
113
- - environment-specific identity file path
89
+ - host-specific identity file path
114
90
  - websocket state
115
91
  - whether `avatarId` is present
116
92
  - whether `authKey` is present
117
93
  - pending request count
118
94
 
95
+ ### kichi_leave
96
+
97
+ ```text
98
+ kichi_leave()
99
+ ```
100
+
101
+ When the user asks to leave Kichi World:
102
+
103
+ 1. Call `kichi_leave`.
104
+ 2. Remove the Kichi heartbeat workflow from workspace `HEARTBEAT.md`.
105
+ 3. Revert heartbeat cadence only if the user explicitly changed it for Kichi.
106
+ 4. Do not claim the plugin removed heartbeat settings automatically.
107
+
119
108
  ### kichi_action
120
109
 
121
- `action` must match the static action list bundled with the plugin package for the selected pose.
122
-
110
+ `action` must match the static action list bundled with the plugin package for the selected pose.
111
+
123
112
  The current action lists are injected into prompt context before the model chooses `kichi_action`.
124
113
 
125
114
  ### kichi_music_album_create
@@ -151,4 +140,4 @@ Plugin runtime directory:
151
140
  Runtime files:
152
141
 
153
142
  - `state.json`
154
- - `env/<env>/identity.json`
143
+ - `hosts/<encoded-host>/identity.json`
@@ -1,10 +1,10 @@
1
1
  # Install Error Handling
2
2
 
3
- If the install command fails, read the error output carefully. Common causes and fixes:
3
+ If `Install the ClawHub package \`clawhub:{packageName}\` via \`openclaw plugins install\`` fails, read the error output carefully. Common causes and fixes:
4
4
 
5
5
  - Permission or access error (`EACCES`): re-run the command with appropriate user-level permissions for the current OS.
6
6
  - npm spawn error (`ENOENT`, `spawn npm`): verify that Node.js (v18+) and npm are installed and available in PATH. On Windows, confirm `where.exe npm` returns a valid path.
7
7
  - File conflict (`EEXIST`): re-run the install command with `--force`.
8
8
  - Network or registry error: check internet connectivity and retry.
9
9
 
10
- After applying the fix, re-run the install command.
10
+ After applying the fix, re-run `openclaw plugins install clawhub:{packageName}`.
@@ -2,7 +2,7 @@
2
2
 
3
3
  ## Session Startup Rule
4
4
 
5
- If `plugins.entries.kichi-forwarder.enabled: true`, ensure workspace `HEARTBEAT.md` includes the Kichi note board workflow:
5
+ If the plugin is installed and enabled, ensure workspace `HEARTBEAT.md` includes the Kichi heartbeat workflow:
6
6
 
7
7
  1. If the workflow snippet is missing, add the snippet from `HEARTBEAT.md Snippet` section below.
8
8
 
@@ -1,13 +1,9 @@
1
1
  # Install
2
2
 
3
- ## Resolve Environment First
3
+ ## Host Rules
4
4
 
5
- Resolve environment before installation, join, or manual switching:
6
-
7
- - If the user does not specify any environment, use production (`prod`).
8
- - `test environment` or `playtest` means `dev`.
9
- - `local environment` or `local` means `local`.
10
- - If the request contains conflicting environment signals, stop and ask for clarification.
5
+ - If the user explicitly includes `host: <value>`, use that host.
6
+ - If the user does not specify a host, use `focus.yahaha.com`.
11
7
 
12
8
  ## Runtime Files
13
9
 
@@ -18,15 +14,15 @@ Persist runtime state to `state.json`:
18
14
 
19
15
  ```json
20
16
  {
21
- "currentEnv": "prod",
17
+ "currentHost": "focus.yahaha.com",
22
18
  "llmRuntimeEnabled": true
23
19
  }
24
20
  ```
25
21
 
26
- Save `avatarId` to the environment-specific `identity.json` before using `kichi_join`:
22
+ Save `avatarId` to the host-specific `identity.json` before using `kichi_join`:
27
23
 
28
- - Linux/macOS: `~/.openclaw/kichi-world/env/<env>/identity.json`
29
- - Windows: `%USERPROFILE%\.openclaw\kichi-world\env\<env>\identity.json`
24
+ - Linux/macOS: `~/.openclaw/kichi-world/hosts/<encoded-host>/identity.json`
25
+ - Windows: `%USERPROFILE%\.openclaw\kichi-world\hosts\<encoded-host>\identity.json`
30
26
 
31
27
  ```json
32
28
  {
@@ -34,44 +30,37 @@ Save `avatarId` to the environment-specific `identity.json` before using `kichi_
34
30
  }
35
31
  ```
36
32
 
37
- ## Install
33
+ ## User Command
34
+
35
+ - Default:
38
36
 
39
- ```bash
40
- openclaw plugins install @yahaha-studio/kichi-forwarder@latest
37
+ ```text
38
+ Install the ClawHub package `clawhub:{packageName}` via `openclaw plugins install`, then follow the plugin's documented Kichi World startup and join flow using avatarId: {avatarId}.
41
39
  ```
42
40
 
43
- ## Join Kichi World Command Path (Non-skippable)
41
+ - With explicit host:
44
42
 
45
- If the user asks:
43
+ ```text
44
+ Install the ClawHub package `clawhub:{packageName}` via `openclaw plugins install`, then follow the plugin's documented Kichi World startup and join flow using avatarId: {avatarId} and host: {host}.
45
+ ```
46
46
 
47
- `Read https://xiaoxinshi001.github.io/yahaha_focus_forwarder_alpha/SKILL.md and connect me to Kichi World with AvatarId: <id>`
47
+ ## Command Execution Flow
48
48
 
49
- execute in this fixed order:
49
+ When the user asks with one of the commands above, execute in this fixed order:
50
50
 
51
- 1. Resolve the environment and write `state.json`.
52
- 2. Ensure plugin is installed and enabled.
53
- 3. Complete `Session Startup Rule` and `First Join Gate` from [heartbeat.md](heartbeat.md).
54
- 4. Call `kichi_status`.
55
- 5. If the current runtime environment does not match the requested one, call `kichi_switch_env`.
56
- 6. If `authKey` is missing, call `kichi_join` with provided `AvatarId`, `botName`, `bio`, and `tags`.
57
- 7. Call `kichi_status` again and confirm connection and auth state.
51
+ 1. Run `openclaw plugins install clawhub:{packageName}`.
52
+ 2. Ensure the plugin is installed and enabled.
53
+ 3. Resolve the host and write `state.json`.
54
+ 4. Update workspace `HEARTBEAT.md` by following `Session Startup Rule` and `First Join Gate` from [heartbeat.md](heartbeat.md). This step is mandatory.
55
+ 5. Call `kichi_status`.
56
+ 6. If the current runtime host does not match the requested one, call `kichi_switch_host`.
57
+ 7. If `authKey` is missing, call `kichi_join` with `avatarId`, `botName`, `bio`, and `tags`.
58
+ 8. Call `kichi_status` again and confirm connection and auth state.
58
59
 
59
60
  ## Required Post-install Integration
60
61
 
61
- 1. Ensure `state.json` is persisted with valid `currentEnv` and `llmRuntimeEnabled`.
62
- 2. Add the Kichi note board workflow snippet to workspace `HEARTBEAT.md` as described in [heartbeat.md](heartbeat.md).
62
+ 1. Ensure `state.json` is persisted with valid `currentHost` and `llmRuntimeEnabled`.
63
+ 2. Add the Kichi heartbeat workflow snippet to workspace `HEARTBEAT.md` as described in [heartbeat.md](heartbeat.md). This step is mandatory.
63
64
  3. Verify tools are callable, for example with `kichi_status`.
64
65
 
65
- This plugin does not edit workspace files automatically. Do not claim plugin-side auto-write of `HEARTBEAT.md`.
66
-
67
- ## Files
68
-
69
- Plugin runtime directory:
70
-
71
- - Linux/macOS: `~/.openclaw/kichi-world/`
72
- - Windows: `%USERPROFILE%\.openclaw\kichi-world\`
73
-
74
- Runtime files:
75
-
76
- - `state.json`
77
- - `env/<env>/identity.json`
66
+ If writing `HEARTBEAT.md` fails, treat the setup and join flow as incomplete.
package/src/config.ts CHANGED
@@ -1,18 +1,7 @@
1
1
  import type { KichiForwarderConfig } from "./types.js";
2
2
 
3
3
  const FIXED_CONFIG: KichiForwarderConfig = {
4
- defaultEnv: "prod",
5
- envs: {
6
- local: {
7
- wsUrl: "ws://127.0.0.1:48870/ws/openclaw",
8
- },
9
- dev: {
10
- wsUrl: "ws://43.106.148.251:48870/ws/openclaw",
11
- },
12
- prod: {
13
- wsUrl: "wss://focus.yahaha.com:48870/ws/openclaw",
14
- },
15
- },
4
+ defaultHost: "focus.yahaha.com",
16
5
  };
17
6
 
18
7
  export function parse(_value: unknown): KichiForwarderConfig {
package/src/service.ts CHANGED
@@ -5,8 +5,6 @@ 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,
10
8
  ClockAction,
11
9
  ClockConfig,
12
10
  ClockPayload,
@@ -17,7 +15,6 @@ import type {
17
15
  JoinAckPayload,
18
16
  JoinPayload,
19
17
  KichiConnectionStatus,
20
- KichiEnv,
21
18
  KichiForwarderConfig,
22
19
  KichiIdentity,
23
20
  KichiState,
@@ -26,11 +23,10 @@ import type {
26
23
  QueryStatusPayload,
27
24
  QueryStatusResultPayload,
28
25
  StatusPayload,
29
- WorkspaceScreenPayload,
30
26
  } from "./types.js";
31
27
 
32
28
  const KICHI_WORLD_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
33
- const ENV_DIR = path.join(KICHI_WORLD_DIR, "env");
29
+ const HOSTS_DIR = path.join(KICHI_WORLD_DIR, "hosts");
34
30
  const STATE_PATH = path.join(KICHI_WORLD_DIR, "state.json");
35
31
  const MAX_NOTEBOARD_TEXT_LENGTH = 200;
36
32
  const DEFAULT_LLM_RUNTIME_ENABLED = true;
@@ -60,7 +56,7 @@ export class KichiForwarderService {
60
56
  private stopped = false;
61
57
  private reconnectTimeout: NodeJS.Timeout | null = null;
62
58
  private identity: KichiIdentity | null = null;
63
- private env: KichiEnv;
59
+ private host: string;
64
60
  private joinResolve: ((result: JoinResult) => void) | null = null;
65
61
  private pendingRequests = new Map<
66
62
  string,
@@ -73,11 +69,11 @@ export class KichiForwarderService {
73
69
  >();
74
70
 
75
71
  constructor(private config: KichiForwarderConfig, private logger: Logger) {
76
- this.env = config.defaultEnv;
72
+ this.host = config.defaultHost;
77
73
  }
78
74
 
79
75
  async start(): Promise<void> {
80
- this.env = this.loadCurrentEnv();
76
+ this.host = this.loadCurrentHost();
81
77
  this.identity = this.loadIdentity();
82
78
  this.stopped = false;
83
79
  this.connect();
@@ -91,13 +87,13 @@ export class KichiForwarderService {
91
87
  this.closeSocket();
92
88
  }
93
89
 
94
- async switchEnvironment(env: KichiEnv): Promise<KichiConnectionStatus> {
95
- this.persistCurrentEnv(env);
96
- this.env = env;
90
+ async switchHost(host: string): Promise<KichiConnectionStatus> {
91
+ this.persistCurrentHost(host);
92
+ this.host = host;
97
93
  this.identity = this.loadIdentity();
98
94
  this.clearReconnectTimeout();
99
- this.rejectPendingRequests(`Kichi websocket switched to ${env}`);
100
- this.failPendingJoin(`Kichi websocket switched to ${env}`);
95
+ this.rejectPendingRequests(`Kichi websocket switched to ${host}`);
96
+ this.failPendingJoin(`Kichi websocket switched to ${host}`);
101
97
  this.closeSocket();
102
98
  if (!this.stopped) {
103
99
  this.connect();
@@ -145,22 +141,6 @@ export class KichiForwarderService {
145
141
  this.ws.send(JSON.stringify(payload));
146
142
  }
147
143
 
148
- sendAvatarCommand(command: AvatarCommand, bubble?: string, log?: string): boolean {
149
- const identity = this.requireIdentity();
150
- if (!identity || this.ws?.readyState !== WebSocket.OPEN) return false;
151
-
152
- const payload: AvatarCommandPayload = {
153
- type: "avatar_command",
154
- avatarId: identity.avatarId,
155
- authKey: identity.authKey,
156
- command,
157
- ...(typeof bubble === "string" && bubble.trim() ? { bubble: bubble.trim() } : {}),
158
- ...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
159
- };
160
- this.ws.send(JSON.stringify(payload));
161
- return true;
162
- }
163
-
164
144
  sendHookNotify(hookType: HookNotifyType, bubble: string): void {
165
145
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
166
146
  const payload: HookNotifyPayload = {
@@ -172,20 +152,6 @@ export class KichiForwarderService {
172
152
  this.ws.send(JSON.stringify(payload));
173
153
  }
174
154
 
175
- sendWorkspaceScreen(text: string, ansi = true): boolean {
176
- const identity = this.requireIdentity();
177
- if (!identity || this.ws?.readyState !== WebSocket.OPEN) return false;
178
- const payload: WorkspaceScreenPayload = {
179
- type: "workspace_screen",
180
- avatarId: identity.avatarId,
181
- authKey: identity.authKey,
182
- text,
183
- ansi,
184
- };
185
- this.ws.send(JSON.stringify(payload));
186
- return true;
187
- }
188
-
189
155
  sendClock(action: ClockAction, clock?: ClockConfig, requestId?: string): boolean {
190
156
  if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return false;
191
157
  if (action === "set" && !clock) return false;
@@ -284,8 +250,8 @@ export class KichiForwarderService {
284
250
 
285
251
  hasValidIdentity(): boolean { return !!this.identity?.avatarId && !!this.identity?.authKey; }
286
252
 
287
- getCurrentEnv(): KichiEnv {
288
- return this.env;
253
+ getCurrentHost(): string {
254
+ return this.host;
289
255
  }
290
256
 
291
257
  getIdentityPath(): string {
@@ -341,7 +307,7 @@ export class KichiForwarderService {
341
307
 
342
308
  getConnectionStatus(): KichiConnectionStatus {
343
309
  return {
344
- env: this.env,
310
+ host: this.host,
345
311
  wsUrl: this.getWsUrl(),
346
312
  identityPath: this.getIdentityPath(),
347
313
  connected: this.isConnected(),
@@ -397,7 +363,7 @@ export class KichiForwarderService {
397
363
 
398
364
  ws.on("open", () => {
399
365
  if (this.ws !== ws) return;
400
- this.logger.info(`Connected to ${wsUrl} (${this.env})`);
366
+ this.logger.info(`Connected to ${wsUrl} (${this.host})`);
401
367
  this.sendRejoinPayload();
402
368
  });
403
369
 
@@ -624,33 +590,40 @@ export class KichiForwarderService {
624
590
  }
625
591
 
626
592
  private getIdentityDir(): string {
627
- return path.join(ENV_DIR, this.env);
593
+ return path.join(HOSTS_DIR, encodeURIComponent(this.host));
628
594
  }
629
595
 
630
596
  private getWsUrl(): string {
631
- return this.config.envs[this.env].wsUrl;
597
+ const protocol = this.isPlainIpHost(this.host) || this.host === "localhost" ? "ws" : "wss";
598
+ return `${protocol}://${this.host}:48870/ws/openclaw`;
599
+ }
600
+
601
+ private isPlainIpHost(host: string): boolean {
602
+ return /^\d{1,3}(\.\d{1,3}){3}$/.test(host)
603
+ || /^\[[0-9a-f:]+\]$/i.test(host)
604
+ || /^[0-9a-f:]+$/i.test(host);
632
605
  }
633
606
 
634
- private loadCurrentEnv(): KichiEnv {
607
+ private loadCurrentHost(): string {
635
608
  try {
636
609
  if (!fs.existsSync(STATE_PATH)) {
637
- this.persistCurrentEnv(this.config.defaultEnv);
638
- return this.config.defaultEnv;
610
+ this.persistCurrentHost(this.config.defaultHost);
611
+ return this.config.defaultHost;
639
612
  }
640
- const data = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8")) as { currentEnv?: unknown };
641
- if (data.currentEnv === "local" || data.currentEnv === "dev" || data.currentEnv === "prod") {
642
- return data.currentEnv;
613
+ const data = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8")) as { currentHost?: unknown };
614
+ if (typeof data.currentHost === "string" && data.currentHost.trim()) {
615
+ return data.currentHost;
643
616
  }
644
- throw new Error(`Invalid currentEnv value in ${STATE_PATH}`);
617
+ throw new Error(`Invalid currentHost value in ${STATE_PATH}`);
645
618
  } catch (error) {
646
- throw new Error(`Failed to load current env: ${error}`);
619
+ throw new Error(`Failed to load current host: ${error}`);
647
620
  }
648
621
  }
649
622
 
650
- private persistCurrentEnv(env: KichiEnv): void {
623
+ private persistCurrentHost(host: string): void {
651
624
  const previousState = this.readStateFile();
652
625
  const nextState: KichiState = {
653
- currentEnv: env,
626
+ currentHost: host,
654
627
  llmRuntimeEnabled: previousState?.llmRuntimeEnabled ?? DEFAULT_LLM_RUNTIME_ENABLED,
655
628
  };
656
629
  fs.mkdirSync(KICHI_WORLD_DIR, { recursive: true, mode: 0o700 });
package/src/types.ts CHANGED
@@ -1,10 +1,7 @@
1
1
  export type KichiForwarderConfig = {
2
- defaultEnv: KichiEnv;
3
- envs: Record<KichiEnv, { wsUrl: string }>;
2
+ defaultHost: string;
4
3
  };
5
4
 
6
- export type KichiEnv = "local" | "dev" | "prod";
7
-
8
5
  export type PoseType = "stand" | "sit" | "lay" | "floor";
9
6
 
10
7
  export type ActionResult = {
@@ -32,7 +29,7 @@ export type Album = {
32
29
  };
33
30
 
34
31
  export type KichiState = {
35
- currentEnv: KichiEnv;
32
+ currentHost: string;
36
33
  llmRuntimeEnabled: boolean;
37
34
  };
38
35
 
@@ -42,7 +39,7 @@ export type KichiIdentity = {
42
39
  };
43
40
 
44
41
  export type KichiConnectionStatus = {
45
- env: KichiEnv;
42
+ host: string;
46
43
  wsUrl: string;
47
44
  identityPath: string;
48
45
  connected: boolean;
@@ -103,17 +100,6 @@ export type StatusPayload = {
103
100
  log: string;
104
101
  };
105
102
 
106
- export type AvatarCommand = "look_at_screen";
107
-
108
- export type AvatarCommandPayload = {
109
- type: "avatar_command";
110
- avatarId: string;
111
- authKey: string;
112
- command: AvatarCommand;
113
- bubble?: string;
114
- log?: string;
115
- };
116
-
117
103
  export type HookNotifyType = "message_received" | "before_send_message";
118
104
 
119
105
  export type HookNotifyPayload = {
@@ -123,14 +109,6 @@ export type HookNotifyPayload = {
123
109
  bubble: string;
124
110
  };
125
111
 
126
- export type WorkspaceScreenPayload = {
127
- type: "workspace_screen";
128
- avatarId: string;
129
- authKey: string;
130
- text: string;
131
- ansi?: boolean;
132
- };
133
-
134
112
  export type ClockAction = "set" | "stop";
135
113
 
136
114
  export type ClockMode = "pomodoro" | "countDown" | "countUp";