@yahaha-studio/kichi-forwarder 0.1.1-beta.9 → 0.1.2-beta.2

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.
@@ -0,0 +1,5 @@
1
+ {
2
+ "steam": "focus-wss.yahaha.com",
3
+ "steam-playtest": "focus-steam-playtest-wss-int.yahaha.com",
4
+ "test": null
5
+ }
package/index.ts CHANGED
@@ -15,11 +15,14 @@ import type {
15
15
  Album,
16
16
  ClockAction,
17
17
  ClockConfig,
18
+ KichiEnvironment,
19
+ KichiEnvironmentsConfig,
18
20
  KichiStaticConfig,
19
21
  PomodoroPhase,
20
22
  PoseType,
21
23
  } from "./src/types.js";
22
24
  const BUNDLED_STATIC_CONFIG_PATH = new URL("./config/kichi-config.json", import.meta.url);
25
+ const BUNDLED_ENVIRONMENTS_CONFIG_PATH = new URL("./config/environments.json", import.meta.url);
23
26
  const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
24
27
  beforePromptBuild: {
25
28
  poseType: "sit",
@@ -200,6 +203,52 @@ function loadStaticConfig(): KichiStaticConfig {
200
203
  return cachedStaticConfig;
201
204
  }
202
205
 
206
+ const VALID_ENVIRONMENTS: KichiEnvironment[] = ["steam", "steam-playtest", "test"];
207
+ let cachedEnvironmentsConfig: KichiEnvironmentsConfig | null = null;
208
+ let cachedEnvironmentsConfigMtime = 0;
209
+
210
+ function getEnvironmentsConfigPath(): string {
211
+ return fileURLToPath(BUNDLED_ENVIRONMENTS_CONFIG_PATH);
212
+ }
213
+
214
+ function loadEnvironmentsConfig(): KichiEnvironmentsConfig {
215
+ const configPath = getEnvironmentsConfigPath();
216
+ const stat = fs.statSync(configPath);
217
+ if (cachedEnvironmentsConfig && stat.mtimeMs === cachedEnvironmentsConfigMtime) {
218
+ return cachedEnvironmentsConfig;
219
+ }
220
+ const raw = JSON.parse(fs.readFileSync(configPath, "utf-8")) as unknown;
221
+ if (!raw || typeof raw !== "object") {
222
+ throw new Error("config/environments.json must be a valid object");
223
+ }
224
+ const config = raw as Record<string, unknown>;
225
+ for (const env of VALID_ENVIRONMENTS) {
226
+ if (!(env in config)) {
227
+ throw new Error(`config/environments.json missing environment "${env}"`);
228
+ }
229
+ const value = config[env];
230
+ if (value !== null && typeof value !== "string") {
231
+ throw new Error(`config/environments.json environment "${env}" must be a string or null`);
232
+ }
233
+ }
234
+ cachedEnvironmentsConfig = config as KichiEnvironmentsConfig;
235
+ cachedEnvironmentsConfigMtime = stat.mtimeMs;
236
+ return cachedEnvironmentsConfig;
237
+ }
238
+
239
+ function isKichiEnvironment(value: unknown): value is KichiEnvironment {
240
+ return typeof value === "string" && VALID_ENVIRONMENTS.includes(value as KichiEnvironment);
241
+ }
242
+
243
+ function resolveEnvironmentHost(environment: KichiEnvironment): { host?: string; error?: string } {
244
+ const config = loadEnvironmentsConfig();
245
+ const configuredHost = config[environment];
246
+ if (typeof configuredHost === "string" && configuredHost.trim()) {
247
+ return { host: configuredHost };
248
+ }
249
+ return { error: `environment "${environment}" has no configured host — update config/environments.json first` };
250
+ }
251
+
203
252
  function sendStatusUpdate(service: KichiForwarderService, status: ActionResult): void {
204
253
  const actionDefinition = getActionDefinition(status.poseType, status.action);
205
254
  service.sendStatus(
@@ -902,18 +951,6 @@ function buildMusicAlbumToolDescription(): string {
902
951
  ].join("\n");
903
952
  }
904
953
 
905
- function isKichiHost(value: unknown): value is string {
906
- if (typeof value !== "string") {
907
- return false;
908
- }
909
- const trimmed = value.trim();
910
- return trimmed.length > 0
911
- && !trimmed.includes("://")
912
- && !trimmed.includes("/")
913
- && !trimmed.includes("?")
914
- && !trimmed.includes("#");
915
- }
916
-
917
954
  function buildMusicTitlesDescription(): string {
918
955
  return [
919
956
  "Track names are injected into this tool schema from the static config bundled with the plugin package.",
@@ -1006,10 +1043,12 @@ function createAgentScopedTool(
1006
1043
  factory: (service: KichiForwarderService, ctx: OpenClawPluginToolContext) => AnyAgentTool,
1007
1044
  ) {
1008
1045
  return (ctx: OpenClawPluginToolContext) => {
1009
- const service = runtimeManager.getRuntime(resolveToolLocator(ctx));
1010
- if (!service) {
1046
+ const locator = resolveToolLocator(ctx);
1047
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1048
+ if (!agentId) {
1011
1049
  throw new Error("Failed to resolve agent-scoped Kichi runtime");
1012
1050
  }
1051
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1013
1052
  return factory(service, ctx);
1014
1053
  };
1015
1054
  }
@@ -1045,6 +1084,11 @@ const plugin = {
1045
1084
  id: "kichi-forwarder",
1046
1085
  start: (ctx) => {
1047
1086
  parse(ctx.config.plugins?.entries?.["kichi-forwarder"]?.config);
1087
+ runtimeManager.setEnvironmentHostResolver((environment) => {
1088
+ const config = loadEnvironmentsConfig();
1089
+ const host = config[environment];
1090
+ return typeof host === "string" && host.trim() ? host : null;
1091
+ });
1048
1092
  runtimeManager.initializeStartupRuntimes();
1049
1093
  },
1050
1094
  stop: () => {
@@ -1124,27 +1168,34 @@ const plugin = {
1124
1168
  return ({
1125
1169
  name: "kichi_switch_host",
1126
1170
  description:
1127
- "Switch Kichi runtime host and reconnect immediately without restarting the gateway.",
1171
+ "Switch Kichi runtime environment and reconnect immediately without restarting the gateway. Host is resolved from config/environments.json.",
1128
1172
  parameters: {
1129
1173
  type: "object",
1130
1174
  properties: {
1131
- host: {
1175
+ environment: {
1132
1176
  type: "string",
1133
- description: "Target Kichi host, for example your.kichi.host or 127.0.0.1",
1177
+ enum: VALID_ENVIRONMENTS,
1178
+ description: "Target environment: steam, steam-playtest, or test",
1134
1179
  },
1135
1180
  },
1136
- required: ["host"],
1181
+ required: ["environment"],
1137
1182
  },
1138
1183
  execute: async (_toolCallId, params) => {
1139
- const host = (params as { host?: unknown } | null)?.host;
1140
- if (!isKichiHost(host)) {
1141
- return { success: false, error: "host must be a non-empty hostname without protocol or path" };
1184
+ const environment = (params as { environment?: unknown } | null)?.environment;
1185
+ if (!isKichiEnvironment(environment)) {
1186
+ return { success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` };
1187
+ }
1188
+
1189
+ const resolved = resolveEnvironmentHost(environment);
1190
+ if (resolved.error) {
1191
+ return { success: false, error: resolved.error };
1142
1192
  }
1143
1193
 
1144
- const status = await service.switchHost(host.trim());
1194
+ const status = await service.switchHost(resolved.host!, environment);
1145
1195
  return {
1146
1196
  success: true,
1147
- host: host.trim(),
1197
+ environment,
1198
+ host: resolved.host,
1148
1199
  status,
1149
1200
  };
1150
1201
  },
@@ -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.1-beta.9",
5
+ "version": "0.1.2-beta.2",
6
6
  "author": "OpenClaw",
7
7
  "skills": ["./skills/kichi-forwarder"],
8
8
  "configSchema": {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.1.1-beta.9",
3
+ "version": "0.1.2-beta.2",
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": "index.ts",
@@ -17,15 +17,19 @@ The following URLs are only used when this skill is first loaded from a remote U
17
17
  - `error.md`: `https://yahaha-studio.github.io/kichi-forwarder/references/error.md`
18
18
  - `heartbeat.md`: `https://yahaha-studio.github.io/kichi-forwarder/references/heartbeat.md`
19
19
 
20
- ## Host Rules
20
+ ## Environment Rules
21
21
 
22
- - Install and connect requests include `host: <value>`.
22
+ Install and connect requests use `on <environment>` syntax. Supported environments:
23
+
24
+ - `steam`: connects to `focus-wss.yahaha.com`
25
+ - `steam-playtest`: connects to `focus-steam-playtest-wss-int.yahaha.com`
26
+ - `test`: no fixed host — ask the user for the current test node host, write it to the plugin's `config/environments.json`, then call `kichi_switch_host`
23
27
 
24
28
  ## Runtime State
25
29
 
26
30
  Runtime data lives under the user home directory and is isolated per OpenClaw agent:
27
31
 
28
- - `kichi-world/agents/<encoded-agent-id>/state.json`: stores that agent's `currentHost` and `llmRuntimeEnabled`
32
+ - `kichi-world/agents/<encoded-agent-id>/state.json`: stores that agent's `currentEnvironment` and `llmRuntimeEnabled`
29
33
  - `kichi-world/agents/<encoded-agent-id>/hosts/<encoded-host>/identity.json`: stores that agent's host-specific `avatarId` and `authKey`
30
34
 
31
35
  ## Remote URL Install Entry
@@ -34,7 +38,7 @@ When this skill is first loaded from:
34
38
 
35
39
  - `https://yahaha-studio.github.io/kichi-forwarder/SKILL.md`
36
40
 
37
- and the user asks to connect/install with `avatarId` and `host`, do not invent a separate install flow in this file.
41
+ and the user asks to connect/install with `avatarId` and an environment (e.g. `on steam-playtest`), do not invent a separate install flow in this file.
38
42
 
39
43
  The install and join flow is defined only in:
40
44
 
@@ -42,14 +46,18 @@ The install and join flow is defined only in:
42
46
 
43
47
  Read that file once to complete the initial install. After local installation, the local `install.md` is used instead.
44
48
 
45
- ## Install Gate
49
+ ## Tool Availability
50
+
51
+ All tools listed in this file (`kichi_join`, `kichi_connection_status`, `kichi_action`, etc.) are available immediately once the plugin is installed and loaded. The recommended order below is sequencing guidance, not an availability gate — you can call any tool at any time.
52
+
53
+ ## Install Flow
46
54
 
47
55
  For install/onboarding/connect requests:
48
56
 
49
57
  - Read and follow `install.md` before starting the install flow.
50
- - Before continuing any install/onboarding/connect flow, check whether `kichi-forwarder` is already installed. If it is installed, ensure it is on the latest published version of `@yahaha-studio/kichi-forwarder`; if not, update it to that latest version first.
58
+ - Attempt to install `@yahaha-studio/kichi-forwarder` directly. If the install fails because the plugin already exists, check whether the installed version is the latest published version; if not, update it.
51
59
  - If plugin installation fails with `429`, treat it as a ClawHub routing failure and switch to the non-ClawHub archive install path from `install.md`/`error.md`. Do not retry the same bare package command.
52
- - Before calling `kichi_connection_status` or `kichi_join`, update workspace `HEARTBEAT.md` by following `heartbeat.md`. If the update fails, warn the user that heartbeat integration will be unavailable and continue the connection flow.
60
+ - Recommended: update workspace `HEARTBEAT.md` by following `heartbeat.md` before calling `kichi_connection_status` or `kichi_join`. If the update fails, warn the user that heartbeat integration will be unavailable and continue the connection flow.
53
61
 
54
62
  ## LLM Runtime
55
63
 
@@ -58,18 +66,16 @@ For install/onboarding/connect requests:
58
66
  - When `true`, sync status uses LLM-driven prompts and may consume extra tokens.
59
67
  - When `false`, sync uses fixed English text.
60
68
 
61
- ## Tool Selection Flow
62
-
63
- Use this order unless the user asks for a different explicit action:
69
+ ## Recommended Tool Order
64
70
 
65
- Install/onboarding requests are the exception: follow `install.md` first.
71
+ Use this order unless the user asks for a different explicit action. For install/onboarding requests, follow `install.md` first.
66
72
 
67
73
  1. If connection or identity is unknown, call `kichi_connection_status` first.
68
- 2. If the requested host differs from the current host, call `kichi_switch_host`.
74
+ 2. If the requested environment differs from the current environment, call `kichi_switch_host` with the target environment.
69
75
  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`.
70
- 4. Otherwise, if no `authKey` is available, call `kichi_join`.
76
+ 4. If no `authKey` is available, call `kichi_join`.
71
77
  5. If `authKey` exists but websocket is not open, call `kichi_rejoin` or wait for automatic reconnect and rejoin.
72
- 6. Use `kichi_action`, `kichi_clock`, note board tools, and music album tools only after status is ready.
78
+ 6. Use `kichi_action`, `kichi_clock`, note board tools, and music album tools after status is ready.
73
79
 
74
80
  ## Tools
75
81
 
@@ -88,10 +94,13 @@ kichi_join(avatarId: "your-avatar-id", botName: "<from IDENTITY.md>", bio: "<fro
88
94
  ### kichi_switch_host
89
95
 
90
96
  ```text
91
- kichi_switch_host(host: "your.kichi.host")
97
+ kichi_switch_host(environment: "steam")
98
+ kichi_switch_host(environment: "test")
92
99
  ```
93
100
 
94
- - `host`: required
101
+ - `environment`: required. One of `steam`, `steam-playtest`, `test`.
102
+ - Host is resolved from `config/environments.json`. If the environment has no configured host (null), the call fails.
103
+ - For `test` environment: ask the user for the test node host, write it to the plugin's `config/environments.json`, then call this tool.
95
104
  - This reloads the host-specific `identity.json` and reconnects the websocket immediately.
96
105
 
97
106
  ### kichi_connection_status
@@ -102,7 +111,7 @@ kichi_connection_status()
102
111
 
103
112
  Use this to confirm:
104
113
 
105
- - current host
114
+ - current environment and host
106
115
  - websocket URL
107
116
  - host-specific identity file path
108
117
  - websocket state
@@ -119,7 +128,7 @@ kichi_leave()
119
128
  When the user asks to leave Kichi World:
120
129
 
121
130
  1. Call `kichi_leave`.
122
- 2. Suggest removing the Kichi heartbeat workflow from workspace `HEARTBEAT.md`. If the user declines, leave it in place.
131
+ 2. Clean up the Kichi heartbeat section from workspace `HEARTBEAT.md`. If the user declines, leave it in place.
123
132
  3. Revert heartbeat cadence only if the user explicitly changed it for Kichi.
124
133
  4. Do not claim the plugin removed heartbeat settings automatically.
125
134
 
@@ -2,15 +2,15 @@
2
2
 
3
3
  ## Session Startup Rule
4
4
 
5
- If the plugin is installed and enabled, update workspace `HEARTBEAT.md` to include the Kichi heartbeat workflow before first `kichi_connection_status` or `kichi_join` in an install/onboarding flow:
5
+ If the plugin is installed and enabled, update workspace `HEARTBEAT.md` to include the Kichi heartbeat workflow as part of the install/onboarding flow (recommended before first `kichi_connection_status` or `kichi_join`, but not required):
6
6
 
7
- 1. If the workflow snippet is missing, add the snippet from `HEARTBEAT.md Snippet` section below.
7
+ 1. If the workflow snippet is missing, add the snippet from `HEARTBEAT.md Snippet` section below. An empty or blank `HEARTBEAT.md` counts as "snippet missing" — not as a read error.
8
8
  2. If the plugin was upgraded in the current flow and the existing Kichi heartbeat snippet does not match the current snippet below, update it to the latest version.
9
9
  3. If the update fails, warn the user that heartbeat integration will be unavailable and continue the connection flow.
10
10
 
11
11
  This startup rule is a recommended step for full heartbeat functionality.
12
12
 
13
- ## First Join Gate
13
+ ## First Join Setup
14
14
 
15
15
  For "join Kichi World" onboarding requests:
16
16
 
@@ -1,8 +1,12 @@
1
1
  # Install
2
2
 
3
- ## Host Rules
3
+ ## Environment Rules
4
4
 
5
- - Install and connect requests include `host: <value>`.
5
+ Install and connect requests use `on <environment>` syntax. Supported environments:
6
+
7
+ - `steam`: connects to `focus-wss.yahaha.com`
8
+ - `steam-playtest`: connects to `focus-steam-playtest-wss-int.yahaha.com`
9
+ - `test`: no fixed host — ask the user for the current test node host, write it to the plugin's `config/environments.json`, then connect
6
10
 
7
11
  ## Runtime Files
8
12
 
@@ -13,12 +17,12 @@ Persist runtime state to the current agent's `state.json`:
13
17
 
14
18
  ```json
15
19
  {
16
- "currentHost": "your.kichi.host",
20
+ "currentEnvironment": "steam",
17
21
  "llmRuntimeEnabled": true
18
22
  }
19
23
  ```
20
24
 
21
- If the current host has no saved `avatarId` yet, save it to the current agent's host-specific `identity.json` before using `kichi_join`:
25
+ If the current host has no saved `avatarId` yet, save it to the current agent's host-specific `identity.json` (this helps `kichi_join` resolve the avatar automatically when `avatarId` is omitted):
22
26
 
23
27
  - Linux/macOS: `~/.openclaw/kichi-world/agents/<encoded-agent-id>/hosts/<encoded-host>/identity.json`
24
28
  - Windows: `%USERPROFILE%\.openclaw\kichi-world\agents\<encoded-agent-id>\hosts\<encoded-host>\identity.json`
@@ -58,7 +62,7 @@ You may also use the exact absolute or relative `.tgz` path that `npm pack` prod
58
62
  This section only applies when the skill is first loaded from a remote URL before local installation. If the user asks:
59
63
 
60
64
  ```text
61
- Read https://yahaha-studio.github.io/kichi-forwarder/SKILL.md and connect me to Kichi World with avatarId: {avatarId} and host: {host}
65
+ Read https://yahaha-studio.github.io/kichi-forwarder/SKILL.md, use AvatarId: {avatarId} to join kichi world on {environment}
62
66
  ```
63
67
 
64
68
  ## Command Execution Flow
@@ -67,21 +71,20 @@ When the user asks with one of the commands above, execute in this fixed order:
67
71
 
68
72
  1. If loaded from a remote URL, read `install.md` and `heartbeat.md` from the published skill URLs first. If installed locally, use the local files.
69
73
  2. Parse `avatarId` from user text (`AvatarId`/`avatarId`, case-insensitive).
70
- 3. Resolve the host and write the current agent's `state.json`.
71
- 4. Check whether `@yahaha-studio/kichi-forwarder` is already installed.
72
- 5. If the plugin already exists, check whether the installed version is the latest published version.
73
- 6. If the plugin is missing, run `openclaw plugins install @yahaha-studio/kichi-forwarder`.
74
- 7. If the plugin is already installed but the version is not the latest, run `openclaw plugins update @yahaha-studio/kichi-forwarder`.
75
- 8. If step 6 fails with `429`, do not retry the same bare package command. Run `npm pack @yahaha-studio/kichi-forwarder`, then install the generated `.tgz` with `openclaw plugins install <tgz-path>`.
76
- 9. If step 7 fails with `429`, do not retry the same bare package command. Run `npm pack @yahaha-studio/kichi-forwarder`, then overwrite the existing install with `openclaw plugins install <tgz-path> --force`.
77
- 10. Ensure the plugin is installed, enabled, and at the latest version.
78
- 11. If the plugin was newly installed or upgraded in this flow, check workspace `HEARTBEAT.md` against the latest Kichi heartbeat requirements before continuing.
79
- 12. Update workspace `HEARTBEAT.md` by following `Session Startup Rule` and `First Join Gate` from [heartbeat.md](heartbeat.md). If the update fails, warn the user and continue.
80
- 13. Call `kichi_connection_status`.
81
- 14. If the current agent runtime host does not match the requested one, call `kichi_switch_host`.
82
- 15. If the current host is still connected with a different `avatarId`, call `kichi_leave` first, then call `kichi_join` with parsed `avatarId`, `botName`, `bio`, and `tags`.
83
- 16. Otherwise, if `authKey` is missing, call `kichi_join` with parsed `avatarId`, `botName`, `bio`, and `tags`.
84
- 17. Call `kichi_connection_status` again and confirm connection and auth state.
74
+ 3. Parse environment from the `on <environment>` part of the command (e.g. `on steam-playtest`). Write the current agent's `state.json`.
75
+ 4. Run `openclaw plugins install @yahaha-studio/kichi-forwarder`.
76
+ 5. If step 4 succeeds, the plugin is installed and up-to-date skip to step 9.
77
+ 6. If step 4 fails because the plugin already exists, check whether the installed version is the latest published version. If the version is already the latest, skip to step 9. If not, run `openclaw plugins update @yahaha-studio/kichi-forwarder`.
78
+ 7. If step 4 fails with `429`, do not retry the same bare package command. Run `npm pack @yahaha-studio/kichi-forwarder`, then install the generated `.tgz` with `openclaw plugins install <tgz-path>`.
79
+ 8. If step 6 update fails with `429`, do not retry the same bare package command. Run `npm pack @yahaha-studio/kichi-forwarder`, then overwrite the existing install with `openclaw plugins install <tgz-path> --force`.
80
+ 9. Ensure the plugin is installed, enabled, and at the latest version.
81
+ 10. If the plugin was newly installed or upgraded in this flow, check workspace `HEARTBEAT.md` against the latest Kichi heartbeat requirements before continuing. An empty or blank `HEARTBEAT.md` means the snippet is missing — treat it the same as "snippet not found", not as a read failure.
82
+ 11. Update workspace `HEARTBEAT.md` by following `Session Startup Rule` and `First Join Setup` from [heartbeat.md](heartbeat.md). If the update fails, warn the user and continue.
83
+ 12. Call `kichi_connection_status`.
84
+ 13. If the current agent runtime environment does not match the requested one, call `kichi_switch_host` with the target environment (and host for test).
85
+ 14. If the current host is still connected with a different `avatarId`, call `kichi_leave` first, then call `kichi_join` with parsed `avatarId`, `botName`, `bio`, and `tags`.
86
+ 15. Otherwise, if `authKey` is missing, call `kichi_join` with parsed `avatarId`, `botName`, `bio`, and `tags`.
87
+ 16. Call `kichi_connection_status` again and confirm connection and auth state.
85
88
 
86
89
  ## Required Post-install Integration
87
90
 
@@ -3,6 +3,7 @@ import os from "node:os";
3
3
  import path from "node:path";
4
4
  import type { Logger } from "openclaw/plugin-sdk";
5
5
  import { KichiForwarderService } from "./service.js";
6
+ import type { KichiEnvironment } from "./types.js";
6
7
 
7
8
  const OPENCLAW_HOME_DIR = path.join(os.homedir(), ".openclaw");
8
9
  const KICHI_WORLD_ROOT_DIR = path.join(OPENCLAW_HOME_DIR, "kichi-world");
@@ -16,9 +17,14 @@ type AgentLocator = {
16
17
 
17
18
  export class KichiRuntimeManager {
18
19
  private services = new Map<string, KichiForwarderService>();
20
+ private resolveEnvironmentHost: ((environment: KichiEnvironment) => string | null) | null = null;
19
21
 
20
22
  constructor(private logger: Logger) {}
21
23
 
24
+ setEnvironmentHostResolver(resolver: (environment: KichiEnvironment) => string | null): void {
25
+ this.resolveEnvironmentHost = resolver;
26
+ }
27
+
22
28
  getRuntime(locator: AgentLocator): KichiForwarderService | null {
23
29
  const agentId = this.resolveAgentId(locator);
24
30
  if (!agentId) {
@@ -112,12 +118,17 @@ export class KichiRuntimeManager {
112
118
  }
113
119
 
114
120
  private createRuntime(agentId: string): KichiForwarderService {
121
+ if (!this.resolveEnvironmentHost) {
122
+ throw new Error("Environment host resolver not set on KichiRuntimeManager");
123
+ }
115
124
  const runtimeDir = this.getRuntimeDir(agentId);
116
125
  fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
117
126
 
127
+ const resolveEnvironmentHost = this.resolveEnvironmentHost;
118
128
  const service = new KichiForwarderService(this.logger, {
119
129
  agentId,
120
130
  runtimeDir,
131
+ resolveEnvironmentHost,
121
132
  });
122
133
  service.start();
123
134
  this.services.set(agentId, service);
package/src/service.ts CHANGED
@@ -17,6 +17,7 @@ import type {
17
17
  JoinAckPayload,
18
18
  JoinPayload,
19
19
  KichiConnectionStatus,
20
+ KichiEnvironment,
20
21
  KichiIdentity,
21
22
  KichiState,
22
23
  LeaveAckPayload,
@@ -53,6 +54,7 @@ export type LeaveResult =
53
54
  type KichiForwarderServiceOptions = {
54
55
  agentId: string;
55
56
  runtimeDir: string;
57
+ resolveEnvironmentHost: (environment: KichiEnvironment) => string | null;
56
58
  };
57
59
 
58
60
  type ConnectReason = "startup" | "switch_host" | "reconnect";
@@ -64,6 +66,7 @@ export class KichiForwarderService {
64
66
  private joinTimeout: NodeJS.Timeout | null = null;
65
67
  private identity: KichiIdentity | null = null;
66
68
  private host: string | null = null;
69
+ private environment: KichiEnvironment | null = null;
67
70
  private joinResolve: ((result: JoinResult) => void) | null = null;
68
71
  private pendingRequests = new Map<
69
72
  string,
@@ -81,7 +84,13 @@ export class KichiForwarderService {
81
84
  ) {}
82
85
 
83
86
  start(): void {
84
- this.host = this.loadCurrentHost();
87
+ const state = this.readStateFile();
88
+ this.environment = (state?.currentEnvironment as KichiEnvironment) ?? null;
89
+ if (this.environment) {
90
+ this.host = this.options.resolveEnvironmentHost(this.environment);
91
+ } else {
92
+ this.host = null;
93
+ }
85
94
  this.identity = this.host ? this.loadIdentity() : null;
86
95
  this.stopped = false;
87
96
  if (this.host) {
@@ -99,9 +108,10 @@ export class KichiForwarderService {
99
108
  this.closeSocket();
100
109
  }
101
110
 
102
- async switchHost(host: string): Promise<KichiConnectionStatus> {
103
- this.persistCurrentHost(host);
111
+ async switchHost(host: string, environment?: KichiEnvironment): Promise<KichiConnectionStatus> {
112
+ this.persistCurrentHost(host, environment);
104
113
  this.host = host;
114
+ this.environment = environment ?? null;
105
115
  this.identity = this.loadIdentity();
106
116
  this.clearReconnectTimeout();
107
117
  this.rejectPendingRequests(`Kichi websocket switched to ${host}`);
@@ -406,6 +416,7 @@ export class KichiForwarderService {
406
416
  wsUrl: this.getWsUrl(),
407
417
  identityPath: this.getIdentityPath(),
408
418
  } : {}),
419
+ ...(this.environment ? { environment: this.environment } : {}),
409
420
  hostConfigured: !!host,
410
421
  connected: this.isConnected(),
411
422
  websocketState: this.getWebsocketState(),
@@ -709,8 +720,10 @@ export class KichiForwarderService {
709
720
  if (!this.host) {
710
721
  throw new Error("No Kichi host configured");
711
722
  }
712
- const protocol = this.isPlainIpHost(this.host) || this.host === "localhost" ? "ws" : "wss";
713
- return `${protocol}://${this.host}:48870/ws/openclaw`;
723
+ const isLocal = this.isPlainIpHost(this.host) || this.host === "localhost";
724
+ const protocol = isLocal ? "ws" : "wss";
725
+ const port = isLocal ? ":48870" : "";
726
+ return `${protocol}://${this.host}${port}/ws/openclaw`;
714
727
  }
715
728
 
716
729
  private isPlainIpHost(host: string): boolean {
@@ -719,26 +732,10 @@ export class KichiForwarderService {
719
732
  || /^[0-9a-f:]+$/i.test(host);
720
733
  }
721
734
 
722
- private loadCurrentHost(): string | null {
723
- try {
724
- const statePath = this.getStatePath();
725
- if (!fs.existsSync(statePath)) {
726
- return null;
727
- }
728
- const data = JSON.parse(fs.readFileSync(statePath, "utf-8")) as { currentHost?: unknown };
729
- if (typeof data.currentHost === "string" && data.currentHost.trim()) {
730
- return data.currentHost;
731
- }
732
- throw new Error(`Invalid currentHost value in ${statePath}`);
733
- } catch (error) {
734
- throw new Error(`Failed to load current host: ${error}`);
735
- }
736
- }
737
-
738
- private persistCurrentHost(host: string): void {
735
+ private persistCurrentHost(host: string, environment?: KichiEnvironment): void {
739
736
  const previousState = this.readStateFile();
740
737
  const nextState: KichiState = {
741
- currentHost: host,
738
+ ...(environment ? { currentEnvironment: environment } : {}),
742
739
  llmRuntimeEnabled: previousState?.llmRuntimeEnabled ?? DEFAULT_LLM_RUNTIME_ENABLED,
743
740
  };
744
741
  fs.mkdirSync(this.options.runtimeDir, { recursive: true, mode: 0o700 });
package/src/types.ts CHANGED
@@ -36,8 +36,12 @@ export type Album = {
36
36
  track: Track[];
37
37
  };
38
38
 
39
+ export type KichiEnvironment = "steam" | "steam-playtest" | "test";
40
+
41
+ export type KichiEnvironmentsConfig = Record<KichiEnvironment, string | null>;
42
+
39
43
  export type KichiState = {
40
- currentHost?: string;
44
+ currentEnvironment?: KichiEnvironment;
41
45
  llmRuntimeEnabled: boolean;
42
46
  };
43
47
 
@@ -51,6 +55,7 @@ export type KichiConnectionStatus = {
51
55
  runtimeDir?: string;
52
56
  statePath?: string;
53
57
  host?: string;
58
+ environment?: KichiEnvironment;
54
59
  wsUrl?: string;
55
60
  identityPath?: string;
56
61
  hostConfigured: boolean;