@yahaha-studio/kichi-forwarder 0.1.1-beta.4 → 0.1.1-beta.6
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 +1 -1
- package/index.ts +33 -8
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/runtime-manager.ts +0 -109
- package/src/service.ts +25 -0
- package/src/types.ts +9 -0
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@ Kichi Forwarder brings your OpenClaw companion into Kichi.
|
|
|
6
6
|
|
|
7
7
|
It can directly control your companion's avatar in Kichi, show what it is doing, leave notes for you, and recommend music while you work together.
|
|
8
8
|
|
|
9
|
-
>
|
|
9
|
+
> [Kichi on Steam](https://store.steampowered.com/app/4427550/Kichi_Focus_Together) — Wishlist now!
|
|
10
10
|
|
|
11
11
|
## Highlights
|
|
12
12
|
|
package/index.ts
CHANGED
|
@@ -954,6 +954,7 @@ function buildKichiActionDescription(): string {
|
|
|
954
954
|
"Directly control the avatar inside Kichi World.",
|
|
955
955
|
"Use this whenever the user explicitly asks you to make the Kichi avatar sit down, stand up, lie down, floor-sit, type, read, meditate, celebrate, or perform another listed animation.",
|
|
956
956
|
"For most work, prefer a sit pose and switch actions as the task moves between stages.",
|
|
957
|
+
"Set verify to true ONLY when the user explicitly requests a pose or action change. The server will confirm whether the avatar actually applied the requested pose. If it could not (e.g. no available seats), the result will contain the actual fallback pose so you can inform the user accurately. During routine sync steps, omit verify.",
|
|
957
958
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
958
959
|
`sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
|
|
959
960
|
`lay actions: ${actions.lay.map((entry) => entry.name).join(", ")}`,
|
|
@@ -985,7 +986,7 @@ function buildKichiIdlePlanDescription(): string {
|
|
|
985
986
|
function buildKichiPrompt(): string {
|
|
986
987
|
return [
|
|
987
988
|
"Kichi avatar control and status sync are available via `kichi_action` and `kichi_clock`.",
|
|
988
|
-
"If the user gives a direct Kichi pose or action request, fulfill it with `kichi_action
|
|
989
|
+
"If the user gives a direct Kichi pose or action request, fulfill it with `kichi_action` and set `verify: true` so you can confirm the avatar actually applied the pose. If the result contains a warning about a fallback, tell the user what actually happened instead of assuming success.",
|
|
989
990
|
"Write the visible reply as a natural user-facing response. Keep `kichi_action`, `kichi_clock`, and sync steps internal and absent from the visible reply.",
|
|
990
991
|
"",
|
|
991
992
|
"kichi_action timing (all required when sync is active):",
|
|
@@ -1212,15 +1213,21 @@ const plugin = {
|
|
|
1212
1213
|
description:
|
|
1213
1214
|
"Short natural first-person sentence under 15 words. Match the language of the bubble and mention the current action and immediate focus.",
|
|
1214
1215
|
},
|
|
1216
|
+
verify: {
|
|
1217
|
+
type: "boolean",
|
|
1218
|
+
description:
|
|
1219
|
+
"Set true ONLY when the user explicitly requests a pose or action. Omit during routine sync steps.",
|
|
1220
|
+
},
|
|
1215
1221
|
},
|
|
1216
1222
|
required: ["poseType", "action"],
|
|
1217
1223
|
},
|
|
1218
1224
|
execute: async (_toolCallId, params) => {
|
|
1219
|
-
const { poseType, action, bubble, log } = (params || {}) as {
|
|
1225
|
+
const { poseType, action, bubble, log, verify } = (params || {}) as {
|
|
1220
1226
|
poseType?: string;
|
|
1221
1227
|
action?: string;
|
|
1222
1228
|
bubble?: string;
|
|
1223
1229
|
log?: string;
|
|
1230
|
+
verify?: boolean;
|
|
1224
1231
|
};
|
|
1225
1232
|
if (!poseType || !action) {
|
|
1226
1233
|
return { success: false, error: "poseType and action parameters are required" };
|
|
@@ -1248,22 +1255,40 @@ const plugin = {
|
|
|
1248
1255
|
|
|
1249
1256
|
const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched.name;
|
|
1250
1257
|
const logText = typeof log === "string" ? log.trim() : "";
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1258
|
+
const playback = getActionPlayback(matched);
|
|
1259
|
+
|
|
1260
|
+
if (verify) {
|
|
1261
|
+
try {
|
|
1262
|
+
const ack = await service.sendStatusVerified(
|
|
1263
|
+
normalizedPoseType, matched.name, bubbleText, logText, playback,
|
|
1264
|
+
);
|
|
1265
|
+
if (ack.warning) {
|
|
1266
|
+
return {
|
|
1267
|
+
success: true,
|
|
1268
|
+
requested: { poseType: normalizedPoseType, action: matched.name },
|
|
1269
|
+
actual: { poseType: ack.poseType, action: ack.action },
|
|
1270
|
+
warning: ack.warning,
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
} catch {
|
|
1274
|
+
// Server not updated or timeout — fall through to normal success
|
|
1275
|
+
}
|
|
1276
|
+
} else {
|
|
1277
|
+
sendStatusUpdate(service, {
|
|
1254
1278
|
poseType: normalizedPoseType,
|
|
1255
1279
|
action: matched.name,
|
|
1256
1280
|
bubble: bubbleText,
|
|
1257
1281
|
log: logText,
|
|
1258
|
-
}
|
|
1259
|
-
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1260
1285
|
return {
|
|
1261
1286
|
success: true,
|
|
1262
1287
|
poseType: normalizedPoseType,
|
|
1263
1288
|
action: matched.name,
|
|
1264
1289
|
bubble: bubbleText,
|
|
1265
1290
|
log: logText,
|
|
1266
|
-
playback
|
|
1291
|
+
playback,
|
|
1267
1292
|
};
|
|
1268
1293
|
},
|
|
1269
1294
|
})));
|
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.1-beta.
|
|
5
|
+
"version": "0.1.1-beta.6",
|
|
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.
|
|
3
|
+
"version": "0.1.1-beta.6",
|
|
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",
|
package/src/runtime-manager.ts
CHANGED
|
@@ -7,10 +7,6 @@ import { KichiForwarderService } from "./service.js";
|
|
|
7
7
|
const OPENCLAW_HOME_DIR = path.join(os.homedir(), ".openclaw");
|
|
8
8
|
const KICHI_WORLD_ROOT_DIR = path.join(OPENCLAW_HOME_DIR, "kichi-world");
|
|
9
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
10
|
|
|
15
11
|
type AgentLocator = {
|
|
16
12
|
agentId?: string;
|
|
@@ -51,8 +47,6 @@ export class KichiRuntimeManager {
|
|
|
51
47
|
}
|
|
52
48
|
|
|
53
49
|
initializeStartupRuntimes(): void {
|
|
54
|
-
this.migrateRuntimeStorage();
|
|
55
|
-
|
|
56
50
|
const rootDir = CANONICAL_AGENT_ROOT_DIR;
|
|
57
51
|
if (!fs.existsSync(rootDir)) {
|
|
58
52
|
return;
|
|
@@ -117,109 +111,6 @@ export class KichiRuntimeManager {
|
|
|
117
111
|
return this.normalizeAgentId(match[1]);
|
|
118
112
|
}
|
|
119
113
|
|
|
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
114
|
private createRuntime(agentId: string): KichiForwarderService {
|
|
224
115
|
const runtimeDir = this.getRuntimeDir(agentId);
|
|
225
116
|
fs.mkdirSync(runtimeDir, { recursive: true, mode: 0o700 });
|
package/src/service.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
PoseType,
|
|
24
24
|
QueryStatusPayload,
|
|
25
25
|
QueryStatusResultPayload,
|
|
26
|
+
StatusAckPayload,
|
|
26
27
|
StatusPayload,
|
|
27
28
|
} from "./types.js";
|
|
28
29
|
|
|
@@ -164,6 +165,30 @@ export class KichiForwarderService {
|
|
|
164
165
|
this.ws.send(JSON.stringify(payload));
|
|
165
166
|
}
|
|
166
167
|
|
|
168
|
+
async sendStatusVerified(
|
|
169
|
+
poseType: PoseType | "",
|
|
170
|
+
action: string,
|
|
171
|
+
bubble: string,
|
|
172
|
+
log: string,
|
|
173
|
+
playback: ActionPlayback,
|
|
174
|
+
): Promise<StatusAckPayload> {
|
|
175
|
+
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
|
|
176
|
+
throw new Error("Kichi websocket is not connected");
|
|
177
|
+
}
|
|
178
|
+
const payload: StatusPayload = {
|
|
179
|
+
type: "status",
|
|
180
|
+
requestId: randomUUID(),
|
|
181
|
+
avatarId: this.identity.avatarId,
|
|
182
|
+
authKey: this.identity.authKey,
|
|
183
|
+
poseType,
|
|
184
|
+
action,
|
|
185
|
+
bubble,
|
|
186
|
+
log,
|
|
187
|
+
playback,
|
|
188
|
+
};
|
|
189
|
+
return this.sendRequest<StatusAckPayload>(payload, "status_ack", 5000);
|
|
190
|
+
}
|
|
191
|
+
|
|
167
192
|
sendHookNotify(hookType: HookNotifyType, bubble: string): void {
|
|
168
193
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
169
194
|
const payload: HookNotifyPayload = {
|
package/src/types.ts
CHANGED
|
@@ -104,6 +104,7 @@ export type LeavePayload = {
|
|
|
104
104
|
|
|
105
105
|
export type StatusPayload = {
|
|
106
106
|
type: "status";
|
|
107
|
+
requestId?: string;
|
|
107
108
|
avatarId: string;
|
|
108
109
|
authKey: string;
|
|
109
110
|
poseType: PoseType | "";
|
|
@@ -113,6 +114,14 @@ export type StatusPayload = {
|
|
|
113
114
|
playback: ActionPlayback;
|
|
114
115
|
};
|
|
115
116
|
|
|
117
|
+
export type StatusAckPayload = {
|
|
118
|
+
type: "status_ack";
|
|
119
|
+
requestId: string;
|
|
120
|
+
poseType: PoseType | "";
|
|
121
|
+
action: string;
|
|
122
|
+
warning?: string;
|
|
123
|
+
};
|
|
124
|
+
|
|
116
125
|
export type HookNotifyType = "message_received" | "before_send_message";
|
|
117
126
|
|
|
118
127
|
export type HookNotifyPayload = {
|