@yahaha-studio/kichi-forwarder 0.1.2-beta.1 → 0.1.2-beta.4
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 +2 -0
- package/dist/index.js +1606 -0
- package/dist/src/config.js +4 -0
- package/dist/src/runtime-manager.js +121 -0
- package/dist/src/service.js +701 -0
- package/dist/src/types.js +1 -0
- package/index.ts +216 -73
- package/openclaw.plugin.json +1 -1
- package/package.json +12 -3
- package/skills/kichi-forwarder/SKILL.md +19 -2
- package/skills/kichi-forwarder/references/error.md +3 -11
- package/skills/kichi-forwarder/references/heartbeat.md +1 -1
- package/skills/kichi-forwarder/references/install.md +12 -22
- package/src/runtime-manager.ts +14 -2
- package/src/service.ts +38 -2
- package/src/types.ts +30 -0
package/index.ts
CHANGED
|
@@ -3,8 +3,9 @@ import { fileURLToPath } from "node:url";
|
|
|
3
3
|
import type {
|
|
4
4
|
AnyAgentTool,
|
|
5
5
|
OpenClawPluginApi,
|
|
6
|
-
OpenClawPluginToolContext,
|
|
7
6
|
} from "openclaw/plugin-sdk";
|
|
7
|
+
import type { OpenClawPluginToolContext } from "openclaw/plugin-sdk/core";
|
|
8
|
+
import { agentCommandFromIngress } from "openclaw/plugin-sdk/agent-runtime";
|
|
8
9
|
import { parse } from "./src/config.js";
|
|
9
10
|
import { KichiRuntimeManager } from "./src/runtime-manager.js";
|
|
10
11
|
import { KichiForwarderService } from "./src/service.js";
|
|
@@ -13,6 +14,8 @@ import type {
|
|
|
13
14
|
ActionPlayback,
|
|
14
15
|
ActionResult,
|
|
15
16
|
Album,
|
|
17
|
+
BotMessageHistoryEntry,
|
|
18
|
+
BotMessageReceivedPayload,
|
|
16
19
|
ClockAction,
|
|
17
20
|
ClockConfig,
|
|
18
21
|
KichiEnvironment,
|
|
@@ -22,6 +25,10 @@ import type {
|
|
|
22
25
|
PoseType,
|
|
23
26
|
} from "./src/types.js";
|
|
24
27
|
const BUNDLED_STATIC_CONFIG_PATH = new URL("./config/kichi-config.json", import.meta.url);
|
|
28
|
+
|
|
29
|
+
function jsonResult(payload: unknown): { content: { type: "text"; text: string }[]; details: unknown } {
|
|
30
|
+
return { content: [{ type: "text", text: JSON.stringify(payload) }], details: payload };
|
|
31
|
+
}
|
|
25
32
|
const BUNDLED_ENVIRONMENTS_CONFIG_PATH = new URL("./config/environments.json", import.meta.url);
|
|
26
33
|
const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
|
|
27
34
|
beforePromptBuild: {
|
|
@@ -1034,6 +1041,8 @@ function buildKichiPrompt(): string {
|
|
|
1034
1041
|
"",
|
|
1035
1042
|
"kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
|
|
1036
1043
|
"",
|
|
1044
|
+
"When sending a bot message, do NOT call kichi_action separately.",
|
|
1045
|
+
"",
|
|
1037
1046
|
"User opt-out, Kichi config/test work, and explicit pose requests take priority over sync.",
|
|
1038
1047
|
].join("\n");
|
|
1039
1048
|
}
|
|
@@ -1070,6 +1079,10 @@ function getRuntimeManager(logger: OpenClawPluginApi["logger"]): KichiRuntimeMan
|
|
|
1070
1079
|
return runtimeManager;
|
|
1071
1080
|
}
|
|
1072
1081
|
|
|
1082
|
+
const BOT_MESSAGE_MAX_DEPTH = 5;
|
|
1083
|
+
const BOT_MESSAGE_COOLDOWN_MS = 5_000;
|
|
1084
|
+
const botMessageCooldowns = new Map<string, number>();
|
|
1085
|
+
|
|
1073
1086
|
const plugin = {
|
|
1074
1087
|
id: "kichi-forwarder",
|
|
1075
1088
|
name: "Kichi Forwarder",
|
|
@@ -1080,6 +1093,47 @@ const plugin = {
|
|
|
1080
1093
|
registerPluginHooks(api, runtimeManager);
|
|
1081
1094
|
const musicTitleEnum = getMusicTitleEnum();
|
|
1082
1095
|
|
|
1096
|
+
runtimeManager.setBotMessageHandler((service, msg) => {
|
|
1097
|
+
if (msg.depth >= BOT_MESSAGE_MAX_DEPTH) {
|
|
1098
|
+
api.logger.info(`[kichi:${service.getAgentId()}] bot_message depth=${msg.depth} >= max=${BOT_MESSAGE_MAX_DEPTH}, ignoring`);
|
|
1099
|
+
return;
|
|
1100
|
+
}
|
|
1101
|
+
const now = Date.now();
|
|
1102
|
+
const cooldownKey = `${service.getAgentId()}:${msg.from}`;
|
|
1103
|
+
const lastReply = botMessageCooldowns.get(cooldownKey) ?? 0;
|
|
1104
|
+
if (now - lastReply < BOT_MESSAGE_COOLDOWN_MS) return;
|
|
1105
|
+
botMessageCooldowns.set(cooldownKey, now);
|
|
1106
|
+
const sessionKey = `agent:${service.getAgentId()}:default`;
|
|
1107
|
+
const history: BotMessageHistoryEntry[] = [
|
|
1108
|
+
...(msg.history ?? []),
|
|
1109
|
+
{ from: msg.from, fromName: msg.fromName, bubble: msg.bubble },
|
|
1110
|
+
];
|
|
1111
|
+
const historyLines = history.map((h) => `${h.fromName}: "${h.bubble}"`);
|
|
1112
|
+
const message = `[Bot conversation]\n${historyLines.join("\n")}\n\nReply with a short bubble (2-5 words). Do not repeat what has already been said. Just output the bubble text, nothing else.`;
|
|
1113
|
+
agentCommandFromIngress({
|
|
1114
|
+
message,
|
|
1115
|
+
sessionKey,
|
|
1116
|
+
agentId: service.getAgentId(),
|
|
1117
|
+
senderIsOwner: false,
|
|
1118
|
+
allowModelOverride: false,
|
|
1119
|
+
deliver: false,
|
|
1120
|
+
}).then((result) => {
|
|
1121
|
+
const replyText = (result.payloads ?? [])
|
|
1122
|
+
.map((p: { text?: string }) => p.text)
|
|
1123
|
+
.filter((t): t is string => typeof t === "string" && t.trim().length > 0)
|
|
1124
|
+
.join(" ")
|
|
1125
|
+
.trim();
|
|
1126
|
+
if (!replyText) {
|
|
1127
|
+
return;
|
|
1128
|
+
}
|
|
1129
|
+
service.sendBotMessage(msg.from, msg.depth + 1, replyText, { history }).catch((sendErr) => {
|
|
1130
|
+
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message send failed: ${sendErr}`);
|
|
1131
|
+
});
|
|
1132
|
+
}).catch((err) => {
|
|
1133
|
+
api.logger.warn(`[kichi:${service.getAgentId()}] bot_message agent run failed: ${err}`);
|
|
1134
|
+
});
|
|
1135
|
+
});
|
|
1136
|
+
|
|
1083
1137
|
api.registerService({
|
|
1084
1138
|
id: "kichi-forwarder",
|
|
1085
1139
|
start: (ctx) => {
|
|
@@ -1102,6 +1156,7 @@ const plugin = {
|
|
|
1102
1156
|
|
|
1103
1157
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1104
1158
|
name: "kichi_join",
|
|
1159
|
+
label: "kichi_join",
|
|
1105
1160
|
description: "Join Kichi world with avatarId, the current bot name, a short bio, and personality tags",
|
|
1106
1161
|
parameters: {
|
|
1107
1162
|
type: "object",
|
|
@@ -1134,27 +1189,28 @@ const plugin = {
|
|
|
1134
1189
|
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
1135
1190
|
}
|
|
1136
1191
|
if (!avatarId) {
|
|
1137
|
-
return { success: false, error: "No avatarId" };
|
|
1192
|
+
return jsonResult({ success: false, error: "No avatarId" });
|
|
1138
1193
|
}
|
|
1139
1194
|
if (!botName) {
|
|
1140
|
-
return { success: false, error: "No botName" };
|
|
1195
|
+
return jsonResult({ success: false, error: "No botName" });
|
|
1141
1196
|
}
|
|
1142
1197
|
if (!bio) {
|
|
1143
|
-
return { success: false, error: "No bio" };
|
|
1198
|
+
return jsonResult({ success: false, error: "No bio" });
|
|
1144
1199
|
}
|
|
1145
1200
|
if (tagsError) {
|
|
1146
|
-
return { success: false, error: tagsError };
|
|
1201
|
+
return jsonResult({ success: false, error: tagsError });
|
|
1147
1202
|
}
|
|
1148
1203
|
const result = await service.join(avatarId, botName, bio, tags ?? []);
|
|
1149
1204
|
if (result.success) {
|
|
1150
|
-
return { success: true, authKey: result.authKey };
|
|
1205
|
+
return jsonResult({ success: true, authKey: result.authKey });
|
|
1151
1206
|
}
|
|
1152
|
-
|
|
1207
|
+
const failure = result as { success: false; error: string; errorCode?: string; errorMessage?: string };
|
|
1208
|
+
return jsonResult({
|
|
1153
1209
|
success: false,
|
|
1154
|
-
error:
|
|
1155
|
-
...(
|
|
1156
|
-
...(
|
|
1157
|
-
};
|
|
1210
|
+
error: failure.error,
|
|
1211
|
+
...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
|
|
1212
|
+
...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
|
|
1213
|
+
});
|
|
1158
1214
|
},
|
|
1159
1215
|
})));
|
|
1160
1216
|
|
|
@@ -1167,6 +1223,7 @@ const plugin = {
|
|
|
1167
1223
|
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1168
1224
|
return ({
|
|
1169
1225
|
name: "kichi_switch_host",
|
|
1226
|
+
label: "kichi_switch_host",
|
|
1170
1227
|
description:
|
|
1171
1228
|
"Switch Kichi runtime environment and reconnect immediately without restarting the gateway. Host is resolved from config/environments.json.",
|
|
1172
1229
|
parameters: {
|
|
@@ -1183,72 +1240,77 @@ const plugin = {
|
|
|
1183
1240
|
execute: async (_toolCallId, params) => {
|
|
1184
1241
|
const environment = (params as { environment?: unknown } | null)?.environment;
|
|
1185
1242
|
if (!isKichiEnvironment(environment)) {
|
|
1186
|
-
return { success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` };
|
|
1243
|
+
return jsonResult({ success: false, error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` });
|
|
1187
1244
|
}
|
|
1188
1245
|
|
|
1189
1246
|
const resolved = resolveEnvironmentHost(environment);
|
|
1190
1247
|
if (resolved.error) {
|
|
1191
|
-
return { success: false, error: resolved.error };
|
|
1248
|
+
return jsonResult({ success: false, error: resolved.error });
|
|
1192
1249
|
}
|
|
1193
1250
|
|
|
1194
1251
|
const status = await service.switchHost(resolved.host!, environment);
|
|
1195
|
-
return {
|
|
1252
|
+
return jsonResult({
|
|
1196
1253
|
success: true,
|
|
1197
1254
|
environment,
|
|
1198
1255
|
host: resolved.host,
|
|
1199
1256
|
status,
|
|
1200
|
-
};
|
|
1257
|
+
});
|
|
1201
1258
|
},
|
|
1202
1259
|
});
|
|
1203
1260
|
});
|
|
1204
1261
|
|
|
1205
1262
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1206
1263
|
name: "kichi_rejoin",
|
|
1264
|
+
label: "kichi_rejoin",
|
|
1207
1265
|
description:
|
|
1208
1266
|
"Request an immediate rejoin attempt with saved avatarId/authKey. Rejoin is also sent automatically after reconnect.",
|
|
1209
1267
|
parameters: { type: "object", properties: {} },
|
|
1210
1268
|
execute: async () => {
|
|
1211
1269
|
const result = service.requestRejoin();
|
|
1212
|
-
return {
|
|
1270
|
+
return jsonResult({
|
|
1213
1271
|
success: result.accepted,
|
|
1214
1272
|
...result,
|
|
1215
1273
|
status: service.getConnectionStatus(),
|
|
1216
|
-
};
|
|
1274
|
+
});
|
|
1217
1275
|
},
|
|
1218
1276
|
})));
|
|
1219
1277
|
|
|
1220
1278
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1221
1279
|
name: "kichi_leave",
|
|
1280
|
+
label: "kichi_leave",
|
|
1222
1281
|
description: "Leave Kichi world",
|
|
1223
1282
|
parameters: { type: "object", properties: {} },
|
|
1224
1283
|
execute: async () => {
|
|
1225
1284
|
const result = await service.leave();
|
|
1226
1285
|
if (result.success) {
|
|
1227
|
-
return { success: true };
|
|
1286
|
+
return jsonResult({ success: true });
|
|
1228
1287
|
}
|
|
1229
|
-
|
|
1288
|
+
const failure = result as { success: false; error: string; errorCode?: string; errorMessage?: string };
|
|
1289
|
+
return jsonResult({
|
|
1230
1290
|
success: false,
|
|
1231
|
-
error:
|
|
1232
|
-
...(
|
|
1233
|
-
...(
|
|
1234
|
-
};
|
|
1291
|
+
error: failure.error,
|
|
1292
|
+
...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
|
|
1293
|
+
...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
|
|
1294
|
+
});
|
|
1235
1295
|
},
|
|
1236
1296
|
})));
|
|
1237
1297
|
|
|
1238
1298
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1239
1299
|
name: "kichi_connection_status",
|
|
1300
|
+
label: "kichi_connection_status",
|
|
1240
1301
|
description: "Check WebSocket connection status and identity readiness only. Does NOT return room info, avatar state, or personnel — use kichi_query_status for that.",
|
|
1241
1302
|
parameters: { type: "object", properties: {} },
|
|
1242
1303
|
execute: async () => {
|
|
1243
|
-
return {
|
|
1304
|
+
return jsonResult({
|
|
1244
1305
|
success: true,
|
|
1245
1306
|
status: service.getConnectionStatus(),
|
|
1246
|
-
};
|
|
1307
|
+
});
|
|
1247
1308
|
},
|
|
1248
1309
|
})));
|
|
1249
1310
|
|
|
1250
1311
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1251
1312
|
name: "kichi_action",
|
|
1313
|
+
label: "kichi_action",
|
|
1252
1314
|
description: buildKichiActionDescription(),
|
|
1253
1315
|
parameters: {
|
|
1254
1316
|
type: "object",
|
|
@@ -1281,27 +1343,27 @@ const plugin = {
|
|
|
1281
1343
|
verify?: boolean;
|
|
1282
1344
|
};
|
|
1283
1345
|
if (!poseType || !action) {
|
|
1284
|
-
return { success: false, error: "poseType and action parameters are required" };
|
|
1346
|
+
return jsonResult({ success: false, error: "poseType and action parameters are required" });
|
|
1285
1347
|
}
|
|
1286
1348
|
if (!["stand", "sit", "lay", "floor"].includes(poseType)) {
|
|
1287
|
-
return {
|
|
1349
|
+
return jsonResult({
|
|
1288
1350
|
success: false,
|
|
1289
1351
|
error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
|
|
1290
|
-
};
|
|
1352
|
+
});
|
|
1291
1353
|
}
|
|
1292
1354
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1293
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1355
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1294
1356
|
}
|
|
1295
1357
|
|
|
1296
1358
|
const normalizedPoseType = poseType as PoseType;
|
|
1297
1359
|
const poseActions = loadStaticConfig().actions[normalizedPoseType];
|
|
1298
1360
|
const matched = poseActions.find((entry) => entry.name.toLowerCase() === action.toLowerCase());
|
|
1299
1361
|
if (!matched) {
|
|
1300
|
-
return {
|
|
1362
|
+
return jsonResult({
|
|
1301
1363
|
success: false,
|
|
1302
1364
|
error: `Unknown action "${action}" for poseType "${poseType}"`,
|
|
1303
1365
|
available: poseActions.map((entry) => entry.name),
|
|
1304
|
-
};
|
|
1366
|
+
});
|
|
1305
1367
|
}
|
|
1306
1368
|
|
|
1307
1369
|
const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched.name;
|
|
@@ -1314,12 +1376,12 @@ const plugin = {
|
|
|
1314
1376
|
normalizedPoseType, matched.name, bubbleText, logText, playback,
|
|
1315
1377
|
);
|
|
1316
1378
|
if (ack.warning) {
|
|
1317
|
-
return {
|
|
1379
|
+
return jsonResult({
|
|
1318
1380
|
success: true,
|
|
1319
1381
|
requested: { poseType: normalizedPoseType, action: matched.name },
|
|
1320
1382
|
actual: { poseType: ack.poseType, action: ack.action },
|
|
1321
1383
|
warning: ack.warning,
|
|
1322
|
-
};
|
|
1384
|
+
});
|
|
1323
1385
|
}
|
|
1324
1386
|
} catch {
|
|
1325
1387
|
// Server not updated or timeout — fall through to normal success
|
|
@@ -1333,18 +1395,19 @@ const plugin = {
|
|
|
1333
1395
|
});
|
|
1334
1396
|
}
|
|
1335
1397
|
|
|
1336
|
-
return {
|
|
1398
|
+
return jsonResult({
|
|
1337
1399
|
success: true,
|
|
1338
1400
|
poseType: normalizedPoseType,
|
|
1339
1401
|
action: matched.name,
|
|
1340
1402
|
bubble: bubbleText,
|
|
1341
1403
|
log: logText,
|
|
1342
1404
|
playback,
|
|
1343
|
-
};
|
|
1405
|
+
});
|
|
1344
1406
|
},
|
|
1345
1407
|
})));
|
|
1346
1408
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1347
1409
|
name: "kichi_idle_plan",
|
|
1410
|
+
label: "kichi_idle_plan",
|
|
1348
1411
|
description: buildKichiIdlePlanDescription(),
|
|
1349
1412
|
parameters: {
|
|
1350
1413
|
type: "object",
|
|
@@ -1424,10 +1487,10 @@ const plugin = {
|
|
|
1424
1487
|
execute: async (_toolCallId, params) => {
|
|
1425
1488
|
const { idlePlan, error } = normalizeIdlePlan(params);
|
|
1426
1489
|
if (!idlePlan) {
|
|
1427
|
-
return { success: false, error: error ?? "Invalid idle plan payload" };
|
|
1490
|
+
return jsonResult({ success: false, error: error ?? "Invalid idle plan payload" });
|
|
1428
1491
|
}
|
|
1429
1492
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1430
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1493
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1431
1494
|
}
|
|
1432
1495
|
const sent = service.sendIdlePlan({
|
|
1433
1496
|
...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
|
|
@@ -1436,20 +1499,21 @@ const plugin = {
|
|
|
1436
1499
|
stages: idlePlan.stages,
|
|
1437
1500
|
});
|
|
1438
1501
|
if (!sent) {
|
|
1439
|
-
return { success: false, error: "Failed to send idle plan payload" };
|
|
1502
|
+
return jsonResult({ success: false, error: "Failed to send idle plan payload" });
|
|
1440
1503
|
}
|
|
1441
|
-
return {
|
|
1504
|
+
return jsonResult({
|
|
1442
1505
|
success: true,
|
|
1443
1506
|
...(idlePlan.requestId ? { requestId: idlePlan.requestId } : {}),
|
|
1444
1507
|
heartbeatIntervalSeconds: idlePlan.heartbeatIntervalSeconds,
|
|
1445
1508
|
totalDurationSeconds: idlePlan.totalDurationSeconds,
|
|
1446
1509
|
goal: idlePlan.goal,
|
|
1447
1510
|
stages: idlePlan.stages,
|
|
1448
|
-
};
|
|
1511
|
+
});
|
|
1449
1512
|
},
|
|
1450
1513
|
})));
|
|
1451
1514
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1452
1515
|
name: "kichi_clock",
|
|
1516
|
+
label: "kichi_clock",
|
|
1453
1517
|
description:
|
|
1454
1518
|
"Send clock commands to Kichi world. Supported actions are set and stop.",
|
|
1455
1519
|
parameters: {
|
|
@@ -1524,44 +1588,45 @@ const plugin = {
|
|
|
1524
1588
|
};
|
|
1525
1589
|
|
|
1526
1590
|
if (!isClockAction(action)) {
|
|
1527
|
-
return {
|
|
1591
|
+
return jsonResult({
|
|
1528
1592
|
success: false,
|
|
1529
1593
|
error: "action must be one of: set, stop",
|
|
1530
|
-
};
|
|
1594
|
+
});
|
|
1531
1595
|
}
|
|
1532
1596
|
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1533
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
1597
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1534
1598
|
}
|
|
1535
1599
|
const normalizedRequestId = typeof requestId === "string" ? requestId : undefined;
|
|
1536
1600
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1537
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1601
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1538
1602
|
}
|
|
1539
1603
|
|
|
1540
1604
|
let normalizedClock: ClockConfig | undefined;
|
|
1541
1605
|
if (action === "set") {
|
|
1542
1606
|
const { clock: nextClock, error } = normalizeClockConfig(clock);
|
|
1543
1607
|
if (!nextClock) {
|
|
1544
|
-
return { success: false, error: error ?? "Invalid clock payload" };
|
|
1608
|
+
return jsonResult({ success: false, error: error ?? "Invalid clock payload" });
|
|
1545
1609
|
}
|
|
1546
1610
|
normalizedClock = nextClock;
|
|
1547
1611
|
}
|
|
1548
1612
|
|
|
1549
1613
|
const sent = service.sendClock(action, normalizedClock, normalizedRequestId);
|
|
1550
1614
|
if (!sent) {
|
|
1551
|
-
return { success: false, error: "Failed to send clock payload" };
|
|
1615
|
+
return jsonResult({ success: false, error: "Failed to send clock payload" });
|
|
1552
1616
|
}
|
|
1553
1617
|
|
|
1554
|
-
return {
|
|
1618
|
+
return jsonResult({
|
|
1555
1619
|
success: true,
|
|
1556
1620
|
action,
|
|
1557
1621
|
requestId: normalizedRequestId,
|
|
1558
1622
|
...(normalizedClock ? { clock: normalizedClock } : {}),
|
|
1559
|
-
};
|
|
1623
|
+
});
|
|
1560
1624
|
},
|
|
1561
1625
|
})));
|
|
1562
1626
|
|
|
1563
1627
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1564
1628
|
name: "kichi_query_status",
|
|
1629
|
+
label: "kichi_query_status",
|
|
1565
1630
|
description:
|
|
1566
1631
|
"Query Kichi room and avatar status — includes room personnel, notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota, and `hasCreatedMusicAlbumToday`. Use this when the user asks to check kichi status, room status, or who is in the room. Also use this before creating a new note or daily recommended music album. For heartbeat planning, use the returned idlePlan as reference when shaping the next idle plan.",
|
|
1567
1632
|
parameters: {
|
|
@@ -1576,28 +1641,29 @@ const plugin = {
|
|
|
1576
1641
|
execute: async (_toolCallId, params) => {
|
|
1577
1642
|
const requestId = (params as { requestId?: unknown } | null)?.requestId;
|
|
1578
1643
|
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1579
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
1644
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1580
1645
|
}
|
|
1581
1646
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1582
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1647
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1583
1648
|
}
|
|
1584
1649
|
|
|
1585
1650
|
try {
|
|
1586
1651
|
const result = await service.queryStatus(
|
|
1587
1652
|
typeof requestId === "string" ? requestId : undefined,
|
|
1588
1653
|
);
|
|
1589
|
-
return result;
|
|
1654
|
+
return jsonResult(result);
|
|
1590
1655
|
} catch (error) {
|
|
1591
|
-
return {
|
|
1656
|
+
return jsonResult({
|
|
1592
1657
|
success: false,
|
|
1593
1658
|
error: `Failed to query status: ${error}`,
|
|
1594
|
-
};
|
|
1659
|
+
});
|
|
1595
1660
|
}
|
|
1596
1661
|
},
|
|
1597
1662
|
})));
|
|
1598
1663
|
|
|
1599
1664
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1600
1665
|
name: "kichi_music_album_create",
|
|
1666
|
+
label: "kichi_music_album_create",
|
|
1601
1667
|
description: buildMusicAlbumToolDescription(),
|
|
1602
1668
|
parameters: {
|
|
1603
1669
|
type: "object",
|
|
@@ -1633,33 +1699,33 @@ const plugin = {
|
|
|
1633
1699
|
};
|
|
1634
1700
|
|
|
1635
1701
|
if (requestId !== undefined && typeof requestId !== "string") {
|
|
1636
|
-
return { success: false, error: "requestId must be a string when provided" };
|
|
1702
|
+
return jsonResult({ success: false, error: "requestId must be a string when provided" });
|
|
1637
1703
|
}
|
|
1638
1704
|
if (typeof albumTitle !== "string" || !albumTitle.trim()) {
|
|
1639
|
-
return { success: false, error: "albumTitle is required" };
|
|
1705
|
+
return jsonResult({ success: false, error: "albumTitle is required" });
|
|
1640
1706
|
}
|
|
1641
1707
|
if (!Array.isArray(musicTitles)) {
|
|
1642
|
-
return { success: false, error: "musicTitles must be an array of track names" };
|
|
1708
|
+
return jsonResult({ success: false, error: "musicTitles must be an array of track names" });
|
|
1643
1709
|
}
|
|
1644
1710
|
|
|
1645
1711
|
const { titles: normalizedTitles, invalidTitles } = normalizeMusicTitles(musicTitles);
|
|
1646
1712
|
if (normalizedTitles.length === 0) {
|
|
1647
|
-
return {
|
|
1713
|
+
return jsonResult({
|
|
1648
1714
|
success: false,
|
|
1649
1715
|
error: "musicTitles must contain at least one valid track name from the static config bundled with the plugin package",
|
|
1650
1716
|
examples: getMusicTitleExamples(),
|
|
1651
|
-
};
|
|
1717
|
+
});
|
|
1652
1718
|
}
|
|
1653
1719
|
if (invalidTitles.length > 0) {
|
|
1654
|
-
return {
|
|
1720
|
+
return jsonResult({
|
|
1655
1721
|
success: false,
|
|
1656
1722
|
error: `Unknown musicTitles: ${invalidTitles.join(", ")}`,
|
|
1657
1723
|
hint: "Use exact track names from the static config bundled with the plugin package",
|
|
1658
1724
|
examples: getMusicTitleExamples(),
|
|
1659
|
-
};
|
|
1725
|
+
});
|
|
1660
1726
|
}
|
|
1661
1727
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1662
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1728
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1663
1729
|
}
|
|
1664
1730
|
|
|
1665
1731
|
try {
|
|
@@ -1668,24 +1734,25 @@ const plugin = {
|
|
|
1668
1734
|
normalizedTitles,
|
|
1669
1735
|
typeof requestId === "string" ? requestId : undefined,
|
|
1670
1736
|
);
|
|
1671
|
-
return {
|
|
1737
|
+
return jsonResult({
|
|
1672
1738
|
success: true,
|
|
1673
1739
|
requestId: normalizedRequestId,
|
|
1674
1740
|
albumTitle: albumTitle.trim(),
|
|
1675
1741
|
musicTitles: normalizedTitles,
|
|
1676
1742
|
trackCount: normalizedTitles.length,
|
|
1677
|
-
};
|
|
1743
|
+
});
|
|
1678
1744
|
} catch (error) {
|
|
1679
|
-
return {
|
|
1745
|
+
return jsonResult({
|
|
1680
1746
|
success: false,
|
|
1681
1747
|
error: `Failed to create music album: ${error}`,
|
|
1682
|
-
};
|
|
1748
|
+
});
|
|
1683
1749
|
}
|
|
1684
1750
|
},
|
|
1685
1751
|
})));
|
|
1686
1752
|
|
|
1687
1753
|
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1688
1754
|
name: "kichi_noteboard_create",
|
|
1755
|
+
label: "kichi_noteboard_create",
|
|
1689
1756
|
description:
|
|
1690
1757
|
"Create a new note on a specific Kichi note board. Prefer querying first so you can avoid duplicate posts and respect rate limits.",
|
|
1691
1758
|
parameters: {
|
|
@@ -1708,29 +1775,105 @@ const plugin = {
|
|
|
1708
1775
|
data?: unknown;
|
|
1709
1776
|
};
|
|
1710
1777
|
if (typeof propId !== "string" || !propId.trim()) {
|
|
1711
|
-
return { success: false, error: "propId is required" };
|
|
1778
|
+
return jsonResult({ success: false, error: "propId is required" });
|
|
1712
1779
|
}
|
|
1713
1780
|
if (typeof data !== "string" || !data.trim()) {
|
|
1714
|
-
return { success: false, error: "data is required" };
|
|
1781
|
+
return jsonResult({ success: false, error: "data is required" });
|
|
1715
1782
|
}
|
|
1716
1783
|
if (data.trim().length > MAX_NOTEBOARD_TEXT_LENGTH) {
|
|
1717
|
-
return {
|
|
1784
|
+
return jsonResult({
|
|
1718
1785
|
success: false,
|
|
1719
1786
|
error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
|
|
1720
|
-
};
|
|
1787
|
+
});
|
|
1721
1788
|
}
|
|
1722
1789
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1723
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1790
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1724
1791
|
}
|
|
1725
1792
|
|
|
1726
1793
|
try {
|
|
1727
1794
|
service.createNotesBoardNote(propId.trim(), data.trim());
|
|
1728
|
-
return { success: true };
|
|
1795
|
+
return jsonResult({ success: true });
|
|
1729
1796
|
} catch (error) {
|
|
1730
|
-
return {
|
|
1797
|
+
return jsonResult({
|
|
1731
1798
|
success: false,
|
|
1732
1799
|
error: `Failed to create note: ${error}`,
|
|
1733
|
-
};
|
|
1800
|
+
});
|
|
1801
|
+
}
|
|
1802
|
+
},
|
|
1803
|
+
})));
|
|
1804
|
+
|
|
1805
|
+
api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
|
|
1806
|
+
name: "kichi_bot_message",
|
|
1807
|
+
label: "kichi_bot_message",
|
|
1808
|
+
description:
|
|
1809
|
+
"Send a message to another bot in the same Kichi world. The bubble is the visible message content. Do not repeat what has already been said in the conversation history. When targeting a specific bot by name, call kichi_query_status first to resolve their avatarId. Only use \"*\" when broadcasting to all bots without a specific target.",
|
|
1810
|
+
parameters: {
|
|
1811
|
+
type: "object",
|
|
1812
|
+
properties: {
|
|
1813
|
+
toAvatarId: {
|
|
1814
|
+
type: "string",
|
|
1815
|
+
description: "Target bot's avatarId (resolve via kichi_query_status if unknown). Use \"*\" only for broadcasting to all bots.",
|
|
1816
|
+
},
|
|
1817
|
+
depth: {
|
|
1818
|
+
type: "number",
|
|
1819
|
+
description: "Conversation depth counter. Increment from the received message's depth.",
|
|
1820
|
+
},
|
|
1821
|
+
bubble: {
|
|
1822
|
+
type: "string",
|
|
1823
|
+
description: "The message to send (2-5 words, visible to everyone). Must not repeat previous messages.",
|
|
1824
|
+
},
|
|
1825
|
+
poseType: {
|
|
1826
|
+
type: "string",
|
|
1827
|
+
enum: ["stand", "sit", "lay", "floor"],
|
|
1828
|
+
description: "Optional pose change when sending.",
|
|
1829
|
+
},
|
|
1830
|
+
action: {
|
|
1831
|
+
type: "string",
|
|
1832
|
+
description: "Optional action to perform when sending.",
|
|
1833
|
+
},
|
|
1834
|
+
log: {
|
|
1835
|
+
type: "string",
|
|
1836
|
+
description: "Optional activity log entry.",
|
|
1837
|
+
},
|
|
1838
|
+
},
|
|
1839
|
+
required: ["toAvatarId", "depth", "bubble"],
|
|
1840
|
+
},
|
|
1841
|
+
execute: async (_toolCallId, params) => {
|
|
1842
|
+
const { toAvatarId, depth, bubble, poseType, action, log } = (params || {}) as {
|
|
1843
|
+
toAvatarId?: string;
|
|
1844
|
+
depth?: number;
|
|
1845
|
+
bubble?: string;
|
|
1846
|
+
poseType?: PoseType;
|
|
1847
|
+
action?: string;
|
|
1848
|
+
log?: string;
|
|
1849
|
+
};
|
|
1850
|
+
if (typeof toAvatarId !== "string" || !toAvatarId.trim()) {
|
|
1851
|
+
return jsonResult({ success: false, error: "toAvatarId is required" });
|
|
1852
|
+
}
|
|
1853
|
+
if (typeof depth !== "number" || depth < 0) {
|
|
1854
|
+
return jsonResult({ success: false, error: "depth must be a non-negative number" });
|
|
1855
|
+
}
|
|
1856
|
+
if (typeof bubble !== "string" || !bubble.trim()) {
|
|
1857
|
+
return jsonResult({ success: false, error: "bubble is required" });
|
|
1858
|
+
}
|
|
1859
|
+
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1860
|
+
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1861
|
+
}
|
|
1862
|
+
try {
|
|
1863
|
+
let playback: ActionPlayback | undefined;
|
|
1864
|
+
if (poseType && action) {
|
|
1865
|
+
const actionDef = getActionDefinition(poseType, action);
|
|
1866
|
+
playback = getActionPlayback(actionDef);
|
|
1867
|
+
}
|
|
1868
|
+
const ack = await service.sendBotMessage(toAvatarId.trim(), depth, bubble.trim(), {
|
|
1869
|
+
poseType,
|
|
1870
|
+
action: action?.trim(),
|
|
1871
|
+
log: log?.trim(),
|
|
1872
|
+
playback,
|
|
1873
|
+
});
|
|
1874
|
+
return jsonResult({ success: true, ...ack });
|
|
1875
|
+
} catch (error) {
|
|
1876
|
+
return jsonResult({ success: false, error: `Failed to send bot message: ${error}` });
|
|
1734
1877
|
}
|
|
1735
1878
|
},
|
|
1736
1879
|
})));
|
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.2-beta.
|
|
5
|
+
"version": "0.1.2-beta.4",
|
|
6
6
|
"author": "OpenClaw",
|
|
7
7
|
"skills": ["./skills/kichi-forwarder"],
|
|
8
8
|
"configSchema": {
|
package/package.json
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yahaha-studio/kichi-forwarder",
|
|
3
|
-
"version": "0.1.2-beta.
|
|
3
|
+
"version": "0.1.2-beta.4",
|
|
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
|
-
"main": "index.
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
7
|
"files": [
|
|
8
8
|
"README.md",
|
|
9
9
|
"assets/",
|
|
10
10
|
"config/",
|
|
11
|
+
"dist/",
|
|
11
12
|
"index.ts",
|
|
12
13
|
"openclaw.plugin.json",
|
|
13
14
|
"skills/",
|
|
@@ -17,6 +18,9 @@
|
|
|
17
18
|
"extensions": [
|
|
18
19
|
"./index.ts"
|
|
19
20
|
],
|
|
21
|
+
"runtimeExtensions": [
|
|
22
|
+
"./dist/index.js"
|
|
23
|
+
],
|
|
20
24
|
"compat": {
|
|
21
25
|
"pluginApi": ">=2026.3.24",
|
|
22
26
|
"minGatewayVersion": "2026.3.24"
|
|
@@ -26,6 +30,10 @@
|
|
|
26
30
|
"pluginSdkVersion": "2026.3.24"
|
|
27
31
|
}
|
|
28
32
|
},
|
|
33
|
+
"scripts": {
|
|
34
|
+
"build": "tsc -p tsconfig.json",
|
|
35
|
+
"prepack": "npm run build"
|
|
36
|
+
},
|
|
29
37
|
"publishConfig": {
|
|
30
38
|
"access": "public"
|
|
31
39
|
},
|
|
@@ -35,6 +43,7 @@
|
|
|
35
43
|
"devDependencies": {
|
|
36
44
|
"@types/node": "^24.3.0",
|
|
37
45
|
"@types/ws": "^8.18.1",
|
|
38
|
-
"openclaw": "2026.3.24"
|
|
46
|
+
"openclaw": "2026.3.24",
|
|
47
|
+
"typescript": "^6.0.3"
|
|
39
48
|
}
|
|
40
49
|
}
|