@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/README.md +6 -6
- package/index.ts +265 -113
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +6 -6
- package/skills/kichi-forwarder/references/install.md +22 -18
- package/src/runtime-manager.ts +240 -0
- package/src/service.ts +134 -44
- package/src/types.ts +3 -0
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.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.
|
|
72
|
-
5. If
|
|
73
|
-
6.
|
|
74
|
-
7. If the plugin
|
|
75
|
-
8.
|
|
76
|
-
9. If `
|
|
77
|
-
10.
|
|
78
|
-
11. If the
|
|
79
|
-
12.
|
|
80
|
-
13.
|
|
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
|
|
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
|
+
}
|