@yahaha-studio/kichi-forwarder 0.1.1-beta.8 → 0.1.2-beta.1
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/config/environments.json +5 -0
- package/index.ts +74 -23
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +27 -18
- package/skills/kichi-forwarder/references/heartbeat.md +4 -4
- package/skills/kichi-forwarder/references/install.md +24 -21
- package/src/runtime-manager.ts +11 -0
- package/src/service.ts +20 -23
- package/src/types.ts +6 -1
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
|
|
1010
|
-
|
|
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
|
|
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
|
-
|
|
1175
|
+
environment: {
|
|
1132
1176
|
type: "string",
|
|
1133
|
-
|
|
1177
|
+
enum: VALID_ENVIRONMENTS,
|
|
1178
|
+
description: "Target environment: steam, steam-playtest, or test",
|
|
1134
1179
|
},
|
|
1135
1180
|
},
|
|
1136
|
-
required: ["
|
|
1181
|
+
required: ["environment"],
|
|
1137
1182
|
},
|
|
1138
1183
|
execute: async (_toolCallId, params) => {
|
|
1139
|
-
const
|
|
1140
|
-
if (!
|
|
1141
|
-
return { success: false, error:
|
|
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
|
|
1194
|
+
const status = await service.switchHost(resolved.host!, environment);
|
|
1145
1195
|
return {
|
|
1146
1196
|
success: true,
|
|
1147
|
-
|
|
1197
|
+
environment,
|
|
1198
|
+
host: resolved.host,
|
|
1148
1199
|
status,
|
|
1149
1200
|
};
|
|
1150
1201
|
},
|
package/openclaw.plugin.json
CHANGED
|
@@ -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.
|
|
5
|
+
"version": "0.1.2-beta.1",
|
|
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.
|
|
3
|
+
"version": "0.1.2-beta.1",
|
|
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
|
-
##
|
|
20
|
+
## Environment Rules
|
|
21
21
|
|
|
22
|
-
|
|
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 `
|
|
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 `
|
|
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
|
-
##
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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
|
|
62
|
-
|
|
63
|
-
Use this order unless the user asks for a different explicit action:
|
|
69
|
+
## Recommended Tool Order
|
|
64
70
|
|
|
65
|
-
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
97
|
+
kichi_switch_host(environment: "steam")
|
|
98
|
+
kichi_switch_host(environment: "test")
|
|
92
99
|
```
|
|
93
100
|
|
|
94
|
-
- `
|
|
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.
|
|
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,7 +2,7 @@
|
|
|
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
|
|
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
7
|
1. If the workflow snippet is missing, add the snippet from `HEARTBEAT.md Snippet` section below.
|
|
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.
|
|
@@ -10,7 +10,7 @@ If the plugin is installed and enabled, update workspace `HEARTBEAT.md` to inclu
|
|
|
10
10
|
|
|
11
11
|
This startup rule is a recommended step for full heartbeat functionality.
|
|
12
12
|
|
|
13
|
-
## First Join
|
|
13
|
+
## First Join Setup
|
|
14
14
|
|
|
15
15
|
For "join Kichi World" onboarding requests:
|
|
16
16
|
|
|
@@ -21,11 +21,11 @@ For "join Kichi World" onboarding requests:
|
|
|
21
21
|
|
|
22
22
|
## Workflow Boundary
|
|
23
23
|
|
|
24
|
-
|
|
24
|
+
The plugin code itself does not write to workspace files. The onboarding instructions above direct the agent to update `HEARTBEAT.md` on behalf of the user as part of the install flow.
|
|
25
25
|
|
|
26
26
|
If user wants recurring note board checks:
|
|
27
27
|
|
|
28
|
-
1.
|
|
28
|
+
1. Update workspace `HEARTBEAT.md`.
|
|
29
29
|
2. Keep the existing OpenClaw heartbeat cadence unless the user explicitly wants a different interval.
|
|
30
30
|
3. Do not claim the plugin edited `HEARTBEAT.md` automatically.
|
|
31
31
|
|
|
@@ -1,8 +1,12 @@
|
|
|
1
1
|
# Install
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Environment Rules
|
|
4
4
|
|
|
5
|
-
|
|
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
|
-
"
|
|
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`
|
|
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
|
|
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.
|
|
71
|
-
4.
|
|
72
|
-
5. If
|
|
73
|
-
6. If the plugin is
|
|
74
|
-
7. If
|
|
75
|
-
8. If step 6 fails with `429`, do not retry the same bare package command. Run `npm pack @yahaha-studio/kichi-forwarder`, then
|
|
76
|
-
9.
|
|
77
|
-
10.
|
|
78
|
-
11.
|
|
79
|
-
12.
|
|
80
|
-
13.
|
|
81
|
-
14. If the current
|
|
82
|
-
15.
|
|
83
|
-
16.
|
|
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.
|
|
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
|
|
|
@@ -95,4 +98,4 @@ If any box is unchecked, the onboarding remains incomplete.
|
|
|
95
98
|
|
|
96
99
|
If the `HEARTBEAT.md` update fails, warn the user that heartbeat integration will be unavailable but do not block the connection flow.
|
|
97
100
|
|
|
98
|
-
|
|
101
|
+
The plugin code does not write to workspace files directly. The agent updates `HEARTBEAT.md` as part of this onboarding flow.
|
package/src/runtime-manager.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
713
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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;
|