@yahaha-studio/kichi-forwarder 0.1.0-beta.9 → 0.1.1-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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.1.0-beta.9",
3
+ "version": "0.1.1-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",
@@ -23,10 +23,10 @@ If this skill is loaded from a remote URL before local installation, use these f
23
23
 
24
24
  ## Runtime State
25
25
 
26
- Runtime data lives under the user home directory:
26
+ Runtime data lives under the user home directory and is isolated per OpenClaw agent:
27
27
 
28
- - `state.json`: stores `currentHost` and `llmRuntimeEnabled`
29
- - `hosts/<encoded-host>/identity.json`: stores host-specific `avatarId` and `authKey`
28
+ - `kichi-world/agents/<encoded-agent-id>/state.json`: stores that agent's `currentHost` and `llmRuntimeEnabled`
29
+ - `kichi-world/agents/<encoded-agent-id>/hosts/<encoded-host>/identity.json`: stores that agent's host-specific `avatarId` and `authKey`
30
30
 
31
31
  ## Remote URL Install Entry
32
32
 
@@ -53,7 +53,7 @@ For install/onboarding/connect requests:
53
53
 
54
54
  ## LLM Runtime
55
55
 
56
- `llmRuntimeEnabled` lives in `state.json`.
56
+ `llmRuntimeEnabled` lives in the current agent's `state.json`.
57
57
 
58
58
  - When `true`, sync status uses LLM-driven prompts and may consume extra tokens.
59
59
  - When `false`, sync uses fixed English text.
@@ -172,8 +172,8 @@ kichi_music_album_create(albumTitle: "Deep Focus Mix", musicTitles: ["Calm Time"
172
172
 
173
173
  Plugin runtime directory:
174
174
 
175
- - Linux/macOS: `~/.openclaw/kichi-world/`
176
- - Windows: `%USERPROFILE%\.openclaw\kichi-world\`
175
+ - Linux/macOS: `~/.openclaw/kichi-world/agents/<encoded-agent-id>/`
176
+ - Windows: `%USERPROFILE%\.openclaw\kichi-world\agents\<encoded-agent-id>\`
177
177
 
178
178
  Runtime files:
179
179
 
@@ -6,10 +6,10 @@
6
6
 
7
7
  ## Runtime Files
8
8
 
9
- Persist runtime state to `state.json`:
9
+ Persist runtime state to the current agent's `state.json`:
10
10
 
11
- - Linux/macOS: `~/.openclaw/kichi-world/state.json`
12
- - Windows: `%USERPROFILE%\.openclaw\kichi-world\state.json`
11
+ - Linux/macOS: `~/.openclaw/kichi-world/agents/<encoded-agent-id>/state.json`
12
+ - Windows: `%USERPROFILE%\.openclaw\kichi-world\agents\<encoded-agent-id>\state.json`
13
13
 
14
14
  ```json
15
15
  {
@@ -18,10 +18,10 @@ Persist runtime state to `state.json`:
18
18
  }
19
19
  ```
20
20
 
21
- Save `avatarId` to the host-specific `identity.json` before using `kichi_join`:
21
+ Save `avatarId` to the current agent's host-specific `identity.json` before using `kichi_join`:
22
22
 
23
- - Linux/macOS: `~/.openclaw/kichi-world/hosts/<encoded-host>/identity.json`
24
- - Windows: `%USERPROFILE%\.openclaw\kichi-world\hosts\<encoded-host>\identity.json`
23
+ - Linux/macOS: `~/.openclaw/kichi-world/agents/<encoded-agent-id>/hosts/<encoded-host>/identity.json`
24
+ - Windows: `%USERPROFILE%\.openclaw\kichi-world\agents\<encoded-agent-id>\hosts\<encoded-host>\identity.json`
25
25
 
26
26
  ```json
27
27
  {
@@ -67,23 +67,27 @@ When the user asks with one of the commands above, execute in this fixed order:
67
67
 
68
68
  1. Read `install.md` and `heartbeat.md` from the published skill URLs before any tool call.
69
69
  2. Parse `avatarId` from user text (`AvatarId`/`avatarId`, case-insensitive).
70
- 3. Resolve the host and write `state.json`.
71
- 4. Run `openclaw plugins install @yahaha-studio/kichi-forwarder`.
72
- 5. 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>`.
73
- 6. Ensure the plugin is installed and enabled.
74
- 7. If the plugin was newly installed or upgraded in this flow, re-check workspace `HEARTBEAT.md` against the latest Kichi heartbeat requirements before continuing.
75
- 8. Update workspace `HEARTBEAT.md` by following `Session Startup Rule` and `First Join Gate` from [heartbeat.md](heartbeat.md).
76
- 9. If `HEARTBEAT.md` was not updated successfully, report setup as incomplete and stop immediately. Do not continue to `kichi_status` or `kichi_join`.
77
- 10. Call `kichi_status`.
78
- 11. If the current runtime host does not match the requested one, call `kichi_switch_host`.
79
- 12. If `authKey` is missing, call `kichi_join` with parsed `avatarId`, `botName`, `bio`, and `tags`.
80
- 13. Call `kichi_status` again and confirm connection and auth state.
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, re-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).
80
+ 13. If `HEARTBEAT.md` was not updated successfully, report setup as incomplete and stop immediately. Do not continue to `kichi_status` or `kichi_join`.
81
+ 14. Call `kichi_status`.
82
+ 15. If the current agent runtime host does not match the requested one, call `kichi_switch_host`.
83
+ 16. If `authKey` is missing, call `kichi_join` with parsed `avatarId`, `botName`, `bio`, and `tags`.
84
+ 17. Call `kichi_status` again and confirm connection and auth state.
81
85
 
82
86
  ## Required Post-install Integration
83
87
 
84
88
  Use this completion checklist:
85
89
 
86
- - [ ] plugin installed and enabled
90
+ - [ ] plugin installed, enabled, and at latest version
87
91
  - [ ] `HEARTBEAT.md` updated with the Kichi heartbeat workflow snippet from [heartbeat.md](heartbeat.md)
88
92
  - [ ] `kichi_status` verified the final connected/auth state
89
93
 
@@ -0,0 +1,240 @@
1
+ import fs from "node:fs";
2
+ import os from "node:os";
3
+ import path from "node:path";
4
+ import type { Logger } from "openclaw/plugin-sdk";
5
+ import { KichiForwarderService } from "./service.js";
6
+
7
+ const OPENCLAW_HOME_DIR = path.join(os.homedir(), ".openclaw");
8
+ const KICHI_WORLD_ROOT_DIR = path.join(OPENCLAW_HOME_DIR, "kichi-world");
9
+ const CANONICAL_AGENT_ROOT_DIR = path.join(KICHI_WORLD_ROOT_DIR, "agents");
10
+ const PREVIOUS_AGENT_ROOT_DIR = path.join(OPENCLAW_HOME_DIR, "kichi-forwarder", "agents");
11
+ const LEGACY_GLOBAL_STATE_PATH = path.join(KICHI_WORLD_ROOT_DIR, "state.json");
12
+ const LEGACY_GLOBAL_HOSTS_DIR = path.join(KICHI_WORLD_ROOT_DIR, "hosts");
13
+ const LEGACY_MIGRATION_AGENT_ID = "main";
14
+
15
+ type AgentLocator = {
16
+ agentId?: string;
17
+ ctxAgentId?: string;
18
+ sessionKey?: string;
19
+ };
20
+
21
+ export class KichiRuntimeManager {
22
+ private services = new Map<string, KichiForwarderService>();
23
+
24
+ constructor(private logger: Logger) {}
25
+
26
+ getRuntime(locator: AgentLocator): KichiForwarderService | null {
27
+ const agentId = this.resolveAgentId(locator);
28
+ if (!agentId) {
29
+ return null;
30
+ }
31
+
32
+ return this.services.get(agentId) ?? null;
33
+ }
34
+
35
+ resolveRuntimeAgentId(locator: AgentLocator): string | null {
36
+ return this.resolveAgentId(locator);
37
+ }
38
+
39
+ createRuntimeForAgent(agentId: string): KichiForwarderService {
40
+ const normalizedAgentId = this.normalizeAgentId(agentId);
41
+ if (!normalizedAgentId) {
42
+ throw new Error("Cannot create Kichi runtime without a valid agentId");
43
+ }
44
+
45
+ const existing = this.services.get(normalizedAgentId);
46
+ if (existing) {
47
+ return existing;
48
+ }
49
+
50
+ return this.createRuntime(normalizedAgentId);
51
+ }
52
+
53
+ initializeStartupRuntimes(): void {
54
+ this.migrateRuntimeStorage();
55
+
56
+ const rootDir = CANONICAL_AGENT_ROOT_DIR;
57
+ if (!fs.existsSync(rootDir)) {
58
+ return;
59
+ }
60
+
61
+ for (const entry of fs.readdirSync(rootDir, { withFileTypes: true })) {
62
+ if (!entry.isDirectory()) {
63
+ continue;
64
+ }
65
+ const runtimeDir = path.join(rootDir, entry.name);
66
+ const statePath = path.join(runtimeDir, "state.json");
67
+ if (!fs.existsSync(statePath)) {
68
+ continue;
69
+ }
70
+
71
+ const agentId = decodeURIComponent(entry.name);
72
+ if (this.services.has(agentId)) {
73
+ continue;
74
+ }
75
+
76
+ this.createRuntime(agentId);
77
+ }
78
+ }
79
+
80
+ stopAll(): void {
81
+ for (const service of this.services.values()) {
82
+ service.stop();
83
+ }
84
+ this.services.clear();
85
+ }
86
+
87
+ private resolveAgentId(locator: AgentLocator): string | null {
88
+ const directAgentId = this.normalizeAgentId(locator.ctxAgentId) ?? this.normalizeAgentId(locator.agentId);
89
+ const sessionAgentId =
90
+ typeof locator.sessionKey === "string" && locator.sessionKey.trim()
91
+ ? this.parseAgentIdFromSessionKey(locator.sessionKey)
92
+ : null;
93
+
94
+ if (sessionAgentId) {
95
+ if (directAgentId && directAgentId !== sessionAgentId) {
96
+ this.logger.error(
97
+ `[kichi] runtime scope mismatch: directAgentId=${directAgentId} sessionAgentId=${sessionAgentId} sessionKey=${locator.sessionKey}`,
98
+ );
99
+ }
100
+ this.logger.debug(`[kichi] resolved agent runtime from sessionKey: ${sessionAgentId}`);
101
+ return sessionAgentId;
102
+ }
103
+
104
+ return directAgentId;
105
+ }
106
+
107
+ private normalizeAgentId(value: unknown): string | null {
108
+ return typeof value === "string" && value.trim() ? value.trim() : null;
109
+ }
110
+
111
+ private parseAgentIdFromSessionKey(sessionKey: string): string | null {
112
+ const trimmed = sessionKey.trim();
113
+ const match = /^agent:([^:]+):/i.exec(trimmed);
114
+ if (!match) {
115
+ return null;
116
+ }
117
+ return this.normalizeAgentId(match[1]);
118
+ }
119
+
120
+ private migrateRuntimeStorage(): void {
121
+ // Temporary startup migration for this release. Remove after users have
122
+ // moved off the legacy/global layout and the temporary kichi-forwarder path.
123
+ this.runMigrationStep("previous-agent-root", () => {
124
+ this.migratePreviousAgentRoot();
125
+ });
126
+ this.runMigrationStep("legacy-global-root", () => {
127
+ this.migrateLegacyGlobalRoot();
128
+ });
129
+ }
130
+
131
+ private migratePreviousAgentRoot(): void {
132
+ if (!fs.existsSync(PREVIOUS_AGENT_ROOT_DIR)) {
133
+ return;
134
+ }
135
+
136
+ if (!fs.existsSync(CANONICAL_AGENT_ROOT_DIR)) {
137
+ fs.mkdirSync(path.dirname(CANONICAL_AGENT_ROOT_DIR), { recursive: true, mode: 0o700 });
138
+ fs.renameSync(PREVIOUS_AGENT_ROOT_DIR, CANONICAL_AGENT_ROOT_DIR);
139
+ this.logger.info(`[kichi:migration] moved ${PREVIOUS_AGENT_ROOT_DIR} to ${CANONICAL_AGENT_ROOT_DIR}`);
140
+ return;
141
+ }
142
+
143
+ for (const entry of fs.readdirSync(PREVIOUS_AGENT_ROOT_DIR, { withFileTypes: true })) {
144
+ const sourcePath = path.join(PREVIOUS_AGENT_ROOT_DIR, entry.name);
145
+ const targetPath = path.join(CANONICAL_AGENT_ROOT_DIR, entry.name);
146
+ this.movePathIntoTarget(sourcePath, targetPath);
147
+ }
148
+ this.removeDirectoryIfEmpty(PREVIOUS_AGENT_ROOT_DIR);
149
+ this.removeDirectoryIfEmpty(path.dirname(PREVIOUS_AGENT_ROOT_DIR));
150
+ }
151
+
152
+ private migrateLegacyGlobalRoot(): void {
153
+ const hasLegacyState = fs.existsSync(LEGACY_GLOBAL_STATE_PATH);
154
+ const hasLegacyHosts = fs.existsSync(LEGACY_GLOBAL_HOSTS_DIR);
155
+ if (!hasLegacyState && !hasLegacyHosts) {
156
+ return;
157
+ }
158
+
159
+ const targetRuntimeDir = this.getRuntimeDir(LEGACY_MIGRATION_AGENT_ID);
160
+ fs.mkdirSync(targetRuntimeDir, { recursive: true, mode: 0o700 });
161
+
162
+ if (hasLegacyState) {
163
+ const targetStatePath = path.join(targetRuntimeDir, "state.json");
164
+ this.movePathIntoTarget(LEGACY_GLOBAL_STATE_PATH, targetStatePath);
165
+ }
166
+
167
+ if (hasLegacyHosts) {
168
+ const targetHostsDir = path.join(targetRuntimeDir, "hosts");
169
+ this.movePathIntoTarget(LEGACY_GLOBAL_HOSTS_DIR, targetHostsDir);
170
+ }
171
+ }
172
+
173
+ private movePathIntoTarget(sourcePath: string, targetPath: string): void {
174
+ if (!fs.existsSync(sourcePath)) {
175
+ return;
176
+ }
177
+
178
+ if (!fs.existsSync(targetPath)) {
179
+ fs.mkdirSync(path.dirname(targetPath), { recursive: true, mode: 0o700 });
180
+ fs.renameSync(sourcePath, targetPath);
181
+ this.logger.info(`[kichi:migration] moved ${sourcePath} to ${targetPath}`);
182
+ return;
183
+ }
184
+
185
+ const sourceStat = fs.lstatSync(sourcePath);
186
+ const targetStat = fs.lstatSync(targetPath);
187
+
188
+ if (sourceStat.isDirectory() && targetStat.isDirectory()) {
189
+ for (const entry of fs.readdirSync(sourcePath, { withFileTypes: true })) {
190
+ const nextSourcePath = path.join(sourcePath, entry.name);
191
+ const nextTargetPath = path.join(targetPath, entry.name);
192
+ this.movePathIntoTarget(nextSourcePath, nextTargetPath);
193
+ }
194
+ this.removeDirectoryIfEmpty(sourcePath);
195
+ return;
196
+ }
197
+
198
+ fs.rmSync(sourcePath, { recursive: sourceStat.isDirectory(), force: true });
199
+ this.logger.warn(`[kichi:migration] dropped ${sourcePath} because target already exists at ${targetPath}`);
200
+ }
201
+
202
+ private removeDirectoryIfEmpty(dirPath: string): void {
203
+ if (!fs.existsSync(dirPath)) {
204
+ return;
205
+ }
206
+ if (!fs.lstatSync(dirPath).isDirectory()) {
207
+ return;
208
+ }
209
+ if (fs.readdirSync(dirPath).length > 0) {
210
+ return;
211
+ }
212
+ fs.rmdirSync(dirPath);
213
+ }
214
+
215
+ private runMigrationStep(label: string, fn: () => void): void {
216
+ try {
217
+ fn();
218
+ } catch (error) {
219
+ this.logger.warn(`[kichi:migration] skipped ${label} due to error: ${String(error)}`);
220
+ }
221
+ }
222
+
223
+ private createRuntime(agentId: string): KichiForwarderService {
224
+ const runtimeDir = this.getRuntimeDir(agentId);
225
+ fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
226
+
227
+ const service = new KichiForwarderService(this.logger, {
228
+ agentId,
229
+ runtimeDir,
230
+ });
231
+ service.start();
232
+ this.services.set(agentId, service);
233
+ this.logger.debug(`[kichi:${agentId}] runtime initialized at ${runtimeDir}`);
234
+ return service;
235
+ }
236
+
237
+ private getRuntimeDir(agentId: string): string {
238
+ return path.join(CANONICAL_AGENT_ROOT_DIR, encodeURIComponent(agentId));
239
+ }
240
+ }