@yahaha-studio/kichi-forwarder 0.1.2-beta.17 → 0.1.2-beta.19
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 -2
- package/dist/index.js +125 -22
- package/dist/src/service.js +4 -2
- package/index.ts +153 -23
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +9 -12
- package/skills/kichi-forwarder/references/install.md +4 -19
- package/src/service.ts +13 -1
- package/src/types.ts +4 -0
package/README.md
CHANGED
|
@@ -31,7 +31,7 @@ Use the bare package name for installation. OpenClaw tries ClawHub first and fal
|
|
|
31
31
|
|
|
32
32
|
Kichi provides the install command and the connection details you need to connect a companion.
|
|
33
33
|
|
|
34
|
-
Get the `
|
|
34
|
+
Get the environment, `avatarId`, and test `host` when using test, then use them with `kichi_join`.
|
|
35
35
|
|
|
36
36
|
## What Your Companion Can Do
|
|
37
37
|
|
|
@@ -48,7 +48,7 @@ Get the `host` and `avatarId` from Kichi, then use them with `kichi_switch_host`
|
|
|
48
48
|
|
|
49
49
|
1. Install the plugin.
|
|
50
50
|
2. Start OpenClaw with the plugin enabled.
|
|
51
|
-
3. Use `
|
|
51
|
+
3. Use `kichi_join` to connect your companion to Kichi.
|
|
52
52
|
4. Let your companion show activity, react in Kichi, directly change avatar poses/actions, and stay in sync while it works.
|
|
53
53
|
5. Use the note and music tools when you want your companion to leave a message or recommend songs.
|
|
54
54
|
|
package/dist/index.js
CHANGED
|
@@ -13,24 +13,28 @@ const FIXED_HOOK_STATUSES = {
|
|
|
13
13
|
poseType: "sit",
|
|
14
14
|
action: "Thinking",
|
|
15
15
|
bubble: "Planning task",
|
|
16
|
+
avatarStatus: "Busy",
|
|
16
17
|
log: "I'm reading the request and getting started.",
|
|
17
18
|
},
|
|
18
19
|
beforeToolCall: {
|
|
19
20
|
poseType: "sit",
|
|
20
21
|
action: "Typing with Keyboard",
|
|
21
22
|
bubble: "Working step",
|
|
23
|
+
avatarStatus: "Busy",
|
|
22
24
|
log: "I'm at the keyboard and working through this step.",
|
|
23
25
|
},
|
|
24
26
|
agentEndSuccess: {
|
|
25
27
|
poseType: "stand",
|
|
26
28
|
action: "Yay",
|
|
27
29
|
bubble: "Task complete",
|
|
30
|
+
avatarStatus: "Idle",
|
|
28
31
|
log: "I wrapped it up and everything landed cleanly.",
|
|
29
32
|
},
|
|
30
33
|
agentEndFailure: {
|
|
31
34
|
poseType: "stand",
|
|
32
35
|
action: "Tired",
|
|
33
36
|
bubble: "Task failed",
|
|
37
|
+
avatarStatus: "Idle",
|
|
34
38
|
log: "I hit a problem here and need another pass.",
|
|
35
39
|
},
|
|
36
40
|
};
|
|
@@ -40,6 +44,7 @@ const MAX_AGENT_END_PREVIEW_WIDTH = 10;
|
|
|
40
44
|
const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
41
45
|
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
42
46
|
const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"];
|
|
47
|
+
const AVATAR_STATUSES = ["Idle", "Busy", "Activities", "Break"];
|
|
43
48
|
let cachedStaticConfig = null;
|
|
44
49
|
let cachedStaticConfigMtime = 0;
|
|
45
50
|
function isAlbumConfig(value) {
|
|
@@ -192,9 +197,26 @@ function resolveEnvironmentHost(environment) {
|
|
|
192
197
|
}
|
|
193
198
|
return { error: `environment "${environment}" has no configured host — update config/environments.json first` };
|
|
194
199
|
}
|
|
200
|
+
function resolveJoinEnvironmentHost(params) {
|
|
201
|
+
if (!isKichiEnvironment(params.environment)) {
|
|
202
|
+
return { error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` };
|
|
203
|
+
}
|
|
204
|
+
if (params.environment === "test") {
|
|
205
|
+
const testHost = typeof params.host === "string" ? params.host.trim() : "";
|
|
206
|
+
if (!testHost) {
|
|
207
|
+
return { error: "host is required for the test environment" };
|
|
208
|
+
}
|
|
209
|
+
return { environment: params.environment, host: testHost };
|
|
210
|
+
}
|
|
211
|
+
const resolved = resolveEnvironmentHost(params.environment);
|
|
212
|
+
if (resolved.error) {
|
|
213
|
+
return { environment: params.environment, error: resolved.error };
|
|
214
|
+
}
|
|
215
|
+
return { environment: params.environment, host: resolved.host };
|
|
216
|
+
}
|
|
195
217
|
function sendStatusUpdate(service, status) {
|
|
196
218
|
const actionDefinition = getActionDefinition(status.poseType, status.action);
|
|
197
|
-
service.sendStatus(status.poseType, actionDefinition.name, status.bubble || status.action, typeof status.log === "string" ? status.log.trim() : "", getActionPlayback(actionDefinition), status.propId);
|
|
219
|
+
service.sendStatus(status.poseType, actionDefinition.name, status.bubble || status.action, typeof status.log === "string" ? status.log.trim() : "", getActionPlayback(actionDefinition), status.avatarStatus, status.propId);
|
|
198
220
|
}
|
|
199
221
|
function syncFixedStatus(service, status) {
|
|
200
222
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
@@ -497,6 +519,12 @@ function isClockAction(value) {
|
|
|
497
519
|
function isIdlePlanPomodoroPhase(value) {
|
|
498
520
|
return IDLE_PLAN_POMODORO_PHASES.includes(String(value));
|
|
499
521
|
}
|
|
522
|
+
function normalizeAvatarStatus(value, fieldPath) {
|
|
523
|
+
if (typeof value !== "string" || !AVATAR_STATUSES.includes(value)) {
|
|
524
|
+
return { error: `${fieldPath} must be one of: ${AVATAR_STATUSES.join(", ")}` };
|
|
525
|
+
}
|
|
526
|
+
return { avatarStatus: value };
|
|
527
|
+
}
|
|
500
528
|
function normalizeIdlePlan(value) {
|
|
501
529
|
if (!isPlainObject(value)) {
|
|
502
530
|
return { error: "idle plan payload must be an object" };
|
|
@@ -527,6 +555,7 @@ function normalizeIdlePlan(value) {
|
|
|
527
555
|
const name = rawStage.name;
|
|
528
556
|
const purpose = rawStage.purpose;
|
|
529
557
|
const pomodoroPhase = rawStage.pomodoroPhase;
|
|
558
|
+
const avatarStatus = rawStage.avatarStatus;
|
|
530
559
|
const durationSeconds = rawStage.durationSeconds;
|
|
531
560
|
const actions = rawStage.actions;
|
|
532
561
|
if (typeof name !== "string" || !name.trim()) {
|
|
@@ -540,6 +569,10 @@ function normalizeIdlePlan(value) {
|
|
|
540
569
|
error: `stages[${stageIndex}].pomodoroPhase must be one of: ${IDLE_PLAN_POMODORO_PHASES.join(", ")}`,
|
|
541
570
|
};
|
|
542
571
|
}
|
|
572
|
+
const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, `stages[${stageIndex}].avatarStatus`);
|
|
573
|
+
if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
|
|
574
|
+
return { error: normalizedAvatarStatus.error ?? `stages[${stageIndex}].avatarStatus is invalid` };
|
|
575
|
+
}
|
|
543
576
|
if (!isPositiveInteger(durationSeconds)) {
|
|
544
577
|
return { error: `stages[${stageIndex}].durationSeconds must be a positive integer` };
|
|
545
578
|
}
|
|
@@ -616,6 +649,7 @@ function normalizeIdlePlan(value) {
|
|
|
616
649
|
name: name.trim(),
|
|
617
650
|
purpose: purpose.trim(),
|
|
618
651
|
pomodoroPhase,
|
|
652
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
619
653
|
durationSeconds,
|
|
620
654
|
actions: normalizedActions,
|
|
621
655
|
});
|
|
@@ -806,6 +840,7 @@ function buildKichiActionDescription(service) {
|
|
|
806
840
|
"Directly control the avatar inside Kichi World.",
|
|
807
841
|
"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.",
|
|
808
842
|
"For most work, prefer a sit pose and switch actions as the task moves between stages.",
|
|
843
|
+
"Set avatarStatus to the current avatar status: Idle, Busy, Activities, or Break.",
|
|
809
844
|
"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.",
|
|
810
845
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
811
846
|
`sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
|
|
@@ -829,9 +864,10 @@ function buildKichiIdlePlanDescription() {
|
|
|
829
864
|
"2. Set the overall goal to that project. Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary as the goal.",
|
|
830
865
|
"3. Break the full heartbeat interval into ordered stages. Each stage purpose must explain what you are actually doing in that stage as part of the same project, not just how you want to feel. Do not switch to unrelated tasks just to use more actions.",
|
|
831
866
|
"4. Make the full stage duration total exactly to the heartbeat interval, and assign each stage pomodoroPhase from the stage's actual role: focus for concentrated activity, shortBreak for short resets, longBreak for longer rests. Do not default the whole idle plan to none. Use none only for a stage that truly has no pomodoro role.",
|
|
832
|
-
"5.
|
|
833
|
-
"6.
|
|
834
|
-
"7.
|
|
867
|
+
"5. Set each stage avatarStatus to the avatar status for that stage: Idle, Busy, Activities, or Break.",
|
|
868
|
+
"6. Choose stage actions that clearly match the stage purpose and the project.",
|
|
869
|
+
"7. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
|
|
870
|
+
"8. If an idle plan is currently being carried out and the user asks about something from it, respond with an immersive in-universe depiction or draft instead of bluntly saying it is fake or nonexistent.",
|
|
835
871
|
"Use your memory to recall what you did in past heartbeats and to stay consistent with your established personality and interests.",
|
|
836
872
|
"Use the same language as the current conversation for goal, purpose, bubble, and log.",
|
|
837
873
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
@@ -855,6 +891,7 @@ function buildKichiPrompt() {
|
|
|
855
891
|
"2. Step switch: call when the task moves into a different stage. Keep the pose aligned with the work, usually staying seated while switching actions within the task as needed.",
|
|
856
892
|
"3. Task end: call BEFORE final reply. Use the order `kichi_action` -> reply.",
|
|
857
893
|
"bubble: 2-5 word companion speech. log: one short natural first-person sentence under 15 words. Match the language of the bubble and mention the current action and immediate focus like a real companion.",
|
|
894
|
+
"avatarStatus: set the current avatar status as Idle, Busy, Activities, or Break.",
|
|
858
895
|
"",
|
|
859
896
|
"kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
|
|
860
897
|
"",
|
|
@@ -948,18 +985,27 @@ const plugin = {
|
|
|
948
985
|
api.registerTool((ctx) => ({
|
|
949
986
|
name: "kichi_join",
|
|
950
987
|
label: "kichi_join",
|
|
951
|
-
description: "Join Kichi world with avatarId, the current bot name, a short bio, and personality tags",
|
|
988
|
+
description: "Join Kichi world in the target environment with avatarId, the current bot name, a short bio, and personality tags. For test, pass host.",
|
|
952
989
|
parameters: {
|
|
953
990
|
type: "object",
|
|
954
991
|
properties: {
|
|
955
992
|
avatarId: { type: "string", description: "Avatar ID to join Kichi world" },
|
|
993
|
+
environment: {
|
|
994
|
+
type: "string",
|
|
995
|
+
enum: VALID_ENVIRONMENTS,
|
|
996
|
+
description: "Target environment. kichi_join switches to this environment before joining.",
|
|
997
|
+
},
|
|
998
|
+
host: {
|
|
999
|
+
type: "string",
|
|
1000
|
+
description: "Test host, required when environment is test and ignored otherwise",
|
|
1001
|
+
},
|
|
956
1002
|
botName: {
|
|
957
1003
|
type: "string",
|
|
958
1004
|
description: "Current bot name to include in the join message",
|
|
959
1005
|
},
|
|
960
1006
|
bio: {
|
|
961
1007
|
type: "string",
|
|
962
|
-
description: "Short bio covering
|
|
1008
|
+
description: "Short bio extracted from SOUL.md, covering persona and idle plan goals if present",
|
|
963
1009
|
},
|
|
964
1010
|
tags: {
|
|
965
1011
|
type: "array",
|
|
@@ -971,7 +1017,7 @@ const plugin = {
|
|
|
971
1017
|
description: "Optional join source identifier. Defaults to Kichi World join-source.json, then openclaw.",
|
|
972
1018
|
},
|
|
973
1019
|
},
|
|
974
|
-
required: ["botName", "bio"],
|
|
1020
|
+
required: ["environment", "avatarId", "botName", "bio"],
|
|
975
1021
|
},
|
|
976
1022
|
execute: async (_toolCallId, params) => {
|
|
977
1023
|
const locator = resolveToolLocator(ctx);
|
|
@@ -980,17 +1026,23 @@ const plugin = {
|
|
|
980
1026
|
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
981
1027
|
}
|
|
982
1028
|
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
983
|
-
|
|
984
|
-
const
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
if (
|
|
989
|
-
|
|
1029
|
+
const p = params;
|
|
1030
|
+
const target = resolveJoinEnvironmentHost({
|
|
1031
|
+
environment: p?.environment,
|
|
1032
|
+
host: p?.host,
|
|
1033
|
+
});
|
|
1034
|
+
if (target.error) {
|
|
1035
|
+
return jsonResult({ success: false, error: target.error });
|
|
990
1036
|
}
|
|
991
|
-
|
|
992
|
-
|
|
1037
|
+
const currentStatus = service.getConnectionStatus();
|
|
1038
|
+
let avatarId = p?.avatarId;
|
|
1039
|
+
if (!avatarId && currentStatus.host === target.host) {
|
|
1040
|
+
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
993
1041
|
}
|
|
1042
|
+
const botName = p?.botName?.trim();
|
|
1043
|
+
const bio = p?.bio?.trim();
|
|
1044
|
+
const rawSource = p?.source;
|
|
1045
|
+
const { tags, error: tagsError } = normalizeJoinTags(p?.tags);
|
|
994
1046
|
if (!botName) {
|
|
995
1047
|
return jsonResult({ success: false, error: "No botName" });
|
|
996
1048
|
}
|
|
@@ -1012,14 +1064,49 @@ const plugin = {
|
|
|
1012
1064
|
if (tagsError) {
|
|
1013
1065
|
return jsonResult({ success: false, error: tagsError });
|
|
1014
1066
|
}
|
|
1067
|
+
let leaveStatus;
|
|
1068
|
+
const shouldLeaveCurrentConnection = currentStatus.connected && currentStatus.hasAuthKey && ((!!currentStatus.host && currentStatus.host !== target.host) ||
|
|
1069
|
+
(currentStatus.host === target.host && !!currentStatus.avatarId && !!avatarId && currentStatus.avatarId !== avatarId));
|
|
1070
|
+
if (shouldLeaveCurrentConnection) {
|
|
1071
|
+
try {
|
|
1072
|
+
leaveStatus = await service.leave();
|
|
1073
|
+
}
|
|
1074
|
+
catch (err) {
|
|
1075
|
+
leaveStatus = {
|
|
1076
|
+
success: false,
|
|
1077
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1078
|
+
};
|
|
1079
|
+
}
|
|
1080
|
+
}
|
|
1081
|
+
let switchStatus;
|
|
1082
|
+
if (target.environment && target.host && service.getCurrentHost() !== target.host) {
|
|
1083
|
+
switchStatus = await service.switchHost(target.host, target.environment);
|
|
1084
|
+
}
|
|
1085
|
+
if (!avatarId) {
|
|
1086
|
+
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
1087
|
+
}
|
|
1088
|
+
if (!avatarId) {
|
|
1089
|
+
return jsonResult({ success: false, error: "No avatarId" });
|
|
1090
|
+
}
|
|
1015
1091
|
const result = await service.join(avatarId, botName, bio, tags ?? [], source);
|
|
1016
1092
|
if (result.success) {
|
|
1017
|
-
return jsonResult({
|
|
1093
|
+
return jsonResult({
|
|
1094
|
+
success: true,
|
|
1095
|
+
authKey: result.authKey,
|
|
1096
|
+
...(target.environment ? { environment: target.environment } : {}),
|
|
1097
|
+
...(target.host ? { host: target.host } : {}),
|
|
1098
|
+
...(switchStatus ? { switchStatus } : {}),
|
|
1099
|
+
...(leaveStatus ? { leaveStatus } : {}),
|
|
1100
|
+
});
|
|
1018
1101
|
}
|
|
1019
1102
|
const failure = result;
|
|
1020
1103
|
return jsonResult({
|
|
1021
1104
|
success: false,
|
|
1022
1105
|
error: failure.error,
|
|
1106
|
+
...(target.environment ? { environment: target.environment } : {}),
|
|
1107
|
+
...(target.host ? { host: target.host } : {}),
|
|
1108
|
+
...(switchStatus ? { switchStatus } : {}),
|
|
1109
|
+
...(leaveStatus ? { leaveStatus } : {}),
|
|
1023
1110
|
...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
|
|
1024
1111
|
...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
|
|
1025
1112
|
});
|
|
@@ -1039,7 +1126,7 @@ const plugin = {
|
|
|
1039
1126
|
},
|
|
1040
1127
|
host: {
|
|
1041
1128
|
type: "string",
|
|
1042
|
-
description: "Test
|
|
1129
|
+
description: "Test host (required for test environment, ignored otherwise)",
|
|
1043
1130
|
},
|
|
1044
1131
|
},
|
|
1045
1132
|
required: ["environment"],
|
|
@@ -1159,6 +1246,11 @@ const plugin = {
|
|
|
1159
1246
|
description: "Action name for the selected pose (for example Sit Nicely, Typing with Keyboard, Reading, High Five, or Meditate)",
|
|
1160
1247
|
},
|
|
1161
1248
|
bubble: { type: "string", description: "Optional bubble text to display (max 5 words)" },
|
|
1249
|
+
avatarStatus: {
|
|
1250
|
+
type: "string",
|
|
1251
|
+
description: "Current avatar status: Idle, Busy, Activities, or Break.",
|
|
1252
|
+
enum: [...AVATAR_STATUSES],
|
|
1253
|
+
},
|
|
1162
1254
|
log: {
|
|
1163
1255
|
type: "string",
|
|
1164
1256
|
description: "Short natural first-person sentence under 15 words. Match the language of the bubble and mention the current action and immediate focus.",
|
|
@@ -1172,7 +1264,7 @@ const plugin = {
|
|
|
1172
1264
|
description: "Optional poseable prop ID from RoomContext.PoseableProps (obtained via kichi_query_status or cached). When specified, the avatar is seated at this prop; when omitted, the server picks the nearest available prop.",
|
|
1173
1265
|
},
|
|
1174
1266
|
},
|
|
1175
|
-
required: ["poseType", "action"],
|
|
1267
|
+
required: ["poseType", "action", "avatarStatus"],
|
|
1176
1268
|
},
|
|
1177
1269
|
execute: async (_toolCallId, params) => {
|
|
1178
1270
|
const locator = resolveToolLocator(ctx);
|
|
@@ -1181,7 +1273,7 @@ const plugin = {
|
|
|
1181
1273
|
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1182
1274
|
}
|
|
1183
1275
|
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1184
|
-
const { poseType, action, bubble, log, verify, propId } = (params || {});
|
|
1276
|
+
const { poseType, action, bubble, avatarStatus, log, verify, propId } = (params || {});
|
|
1185
1277
|
if (!poseType || !action) {
|
|
1186
1278
|
return jsonResult({ success: false, error: "poseType and action parameters are required" });
|
|
1187
1279
|
}
|
|
@@ -1191,6 +1283,10 @@ const plugin = {
|
|
|
1191
1283
|
error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
|
|
1192
1284
|
});
|
|
1193
1285
|
}
|
|
1286
|
+
const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, "avatarStatus");
|
|
1287
|
+
if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
|
|
1288
|
+
return jsonResult({ success: false, error: normalizedAvatarStatus.error ?? "avatarStatus is invalid" });
|
|
1289
|
+
}
|
|
1194
1290
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1195
1291
|
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1196
1292
|
}
|
|
@@ -1209,7 +1305,7 @@ const plugin = {
|
|
|
1209
1305
|
const playback = getActionPlayback(matched);
|
|
1210
1306
|
if (verify) {
|
|
1211
1307
|
try {
|
|
1212
|
-
const ack = await service.sendStatusVerified(normalizedPoseType, matched.name, bubbleText, logText, playback, propId);
|
|
1308
|
+
const ack = await service.sendStatusVerified(normalizedPoseType, matched.name, bubbleText, logText, playback, normalizedAvatarStatus.avatarStatus, propId);
|
|
1213
1309
|
if (ack.warning) {
|
|
1214
1310
|
return jsonResult({
|
|
1215
1311
|
success: true,
|
|
@@ -1229,6 +1325,7 @@ const plugin = {
|
|
|
1229
1325
|
action: matched.name,
|
|
1230
1326
|
bubble: bubbleText,
|
|
1231
1327
|
log: logText,
|
|
1328
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
1232
1329
|
propId,
|
|
1233
1330
|
});
|
|
1234
1331
|
}
|
|
@@ -1238,6 +1335,7 @@ const plugin = {
|
|
|
1238
1335
|
action: matched.name,
|
|
1239
1336
|
bubble: bubbleText,
|
|
1240
1337
|
log: logText,
|
|
1338
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
1241
1339
|
playback,
|
|
1242
1340
|
});
|
|
1243
1341
|
},
|
|
@@ -1334,6 +1432,11 @@ const plugin = {
|
|
|
1334
1432
|
description: "Pomodoro phase for this stage: focus, shortBreak, longBreak, or none. Set it from the stage's actual role. Treat none as exceptional, not the default for the whole plan.",
|
|
1335
1433
|
enum: [...IDLE_PLAN_POMODORO_PHASES],
|
|
1336
1434
|
},
|
|
1435
|
+
avatarStatus: {
|
|
1436
|
+
type: "string",
|
|
1437
|
+
description: "Avatar status for this stage: Idle, Busy, Activities, or Break.",
|
|
1438
|
+
enum: [...AVATAR_STATUSES],
|
|
1439
|
+
},
|
|
1337
1440
|
durationSeconds: {
|
|
1338
1441
|
type: "number",
|
|
1339
1442
|
description: "Required duration in seconds for this stage.",
|
|
@@ -1373,7 +1476,7 @@ const plugin = {
|
|
|
1373
1476
|
},
|
|
1374
1477
|
},
|
|
1375
1478
|
},
|
|
1376
|
-
required: ["name", "purpose", "pomodoroPhase", "durationSeconds", "actions"],
|
|
1479
|
+
required: ["name", "purpose", "pomodoroPhase", "avatarStatus", "durationSeconds", "actions"],
|
|
1377
1480
|
},
|
|
1378
1481
|
},
|
|
1379
1482
|
},
|
package/dist/src/service.js
CHANGED
|
@@ -98,7 +98,7 @@ export class KichiForwarderService {
|
|
|
98
98
|
}, 10000);
|
|
99
99
|
});
|
|
100
100
|
}
|
|
101
|
-
sendStatus(poseType, action, bubble, log, playback, propId) {
|
|
101
|
+
sendStatus(poseType, action, bubble, log, playback, avatarStatus, propId) {
|
|
102
102
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN)
|
|
103
103
|
return;
|
|
104
104
|
const payload = {
|
|
@@ -110,11 +110,12 @@ export class KichiForwarderService {
|
|
|
110
110
|
bubble,
|
|
111
111
|
log,
|
|
112
112
|
playback,
|
|
113
|
+
avatarStatus,
|
|
113
114
|
...(propId ? { propId } : {}),
|
|
114
115
|
};
|
|
115
116
|
this.ws.send(JSON.stringify(payload));
|
|
116
117
|
}
|
|
117
|
-
async sendStatusVerified(poseType, action, bubble, log, playback, propId) {
|
|
118
|
+
async sendStatusVerified(poseType, action, bubble, log, playback, avatarStatus, propId) {
|
|
118
119
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
|
|
119
120
|
throw new Error("Kichi websocket is not connected");
|
|
120
121
|
}
|
|
@@ -128,6 +129,7 @@ export class KichiForwarderService {
|
|
|
128
129
|
bubble,
|
|
129
130
|
log,
|
|
130
131
|
playback,
|
|
132
|
+
avatarStatus,
|
|
131
133
|
...(propId ? { propId } : {}),
|
|
132
134
|
};
|
|
133
135
|
return this.sendRequest(payload, "status_ack", 5000);
|
package/index.ts
CHANGED
|
@@ -13,6 +13,7 @@ import type {
|
|
|
13
13
|
ActionPlayback,
|
|
14
14
|
ActionResult,
|
|
15
15
|
Album,
|
|
16
|
+
AvatarStatus,
|
|
16
17
|
BotMessageHistoryEntry,
|
|
17
18
|
BotMessageReceivedPayload,
|
|
18
19
|
ClockAction,
|
|
@@ -34,24 +35,28 @@ const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
|
|
|
34
35
|
poseType: "sit",
|
|
35
36
|
action: "Thinking",
|
|
36
37
|
bubble: "Planning task",
|
|
38
|
+
avatarStatus: "Busy",
|
|
37
39
|
log: "I'm reading the request and getting started.",
|
|
38
40
|
},
|
|
39
41
|
beforeToolCall: {
|
|
40
42
|
poseType: "sit",
|
|
41
43
|
action: "Typing with Keyboard",
|
|
42
44
|
bubble: "Working step",
|
|
45
|
+
avatarStatus: "Busy",
|
|
43
46
|
log: "I'm at the keyboard and working through this step.",
|
|
44
47
|
},
|
|
45
48
|
agentEndSuccess: {
|
|
46
49
|
poseType: "stand",
|
|
47
50
|
action: "Yay",
|
|
48
51
|
bubble: "Task complete",
|
|
52
|
+
avatarStatus: "Idle",
|
|
49
53
|
log: "I wrapped it up and everything landed cleanly.",
|
|
50
54
|
},
|
|
51
55
|
agentEndFailure: {
|
|
52
56
|
poseType: "stand",
|
|
53
57
|
action: "Tired",
|
|
54
58
|
bubble: "Task failed",
|
|
59
|
+
avatarStatus: "Idle",
|
|
55
60
|
log: "I hit a problem here and need another pass.",
|
|
56
61
|
},
|
|
57
62
|
};
|
|
@@ -62,9 +67,11 @@ const MAX_AGENT_END_PREVIEW_WIDTH = 10;
|
|
|
62
67
|
const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
63
68
|
const DEFAULT_GLANCE_DURATION_SECONDS = 1.8;
|
|
64
69
|
const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"] as const;
|
|
70
|
+
const AVATAR_STATUSES = ["Idle", "Busy", "Activities", "Break"] as const;
|
|
65
71
|
let cachedStaticConfig: KichiStaticConfig | null = null;
|
|
66
72
|
let cachedStaticConfigMtime = 0;
|
|
67
73
|
|
|
74
|
+
type AvatarStatusName = typeof AVATAR_STATUSES[number];
|
|
68
75
|
type IdlePlanPomodoroPhase = typeof IDLE_PLAN_POMODORO_PHASES[number];
|
|
69
76
|
type IdlePlanAction = {
|
|
70
77
|
poseType: PoseType;
|
|
@@ -82,6 +89,7 @@ type IdlePlan = {
|
|
|
82
89
|
name: string;
|
|
83
90
|
purpose: string;
|
|
84
91
|
pomodoroPhase: IdlePlanPomodoroPhase;
|
|
92
|
+
avatarStatus: AvatarStatus;
|
|
85
93
|
durationSeconds: number;
|
|
86
94
|
actions: IdlePlanAction[];
|
|
87
95
|
}>;
|
|
@@ -256,6 +264,27 @@ function resolveEnvironmentHost(environment: KichiEnvironment): { host?: string;
|
|
|
256
264
|
return { error: `environment "${environment}" has no configured host — update config/environments.json first` };
|
|
257
265
|
}
|
|
258
266
|
|
|
267
|
+
function resolveJoinEnvironmentHost(params: {
|
|
268
|
+
environment?: unknown;
|
|
269
|
+
host?: unknown;
|
|
270
|
+
}): { environment?: KichiEnvironment; host?: string; error?: string } {
|
|
271
|
+
if (!isKichiEnvironment(params.environment)) {
|
|
272
|
+
return { error: `environment must be one of: ${VALID_ENVIRONMENTS.join(", ")}` };
|
|
273
|
+
}
|
|
274
|
+
if (params.environment === "test") {
|
|
275
|
+
const testHost = typeof params.host === "string" ? params.host.trim() : "";
|
|
276
|
+
if (!testHost) {
|
|
277
|
+
return { error: "host is required for the test environment" };
|
|
278
|
+
}
|
|
279
|
+
return { environment: params.environment, host: testHost };
|
|
280
|
+
}
|
|
281
|
+
const resolved = resolveEnvironmentHost(params.environment);
|
|
282
|
+
if (resolved.error) {
|
|
283
|
+
return { environment: params.environment, error: resolved.error };
|
|
284
|
+
}
|
|
285
|
+
return { environment: params.environment, host: resolved.host };
|
|
286
|
+
}
|
|
287
|
+
|
|
259
288
|
function sendStatusUpdate(service: KichiForwarderService, status: ActionResult): void {
|
|
260
289
|
const actionDefinition = getActionDefinition(status.poseType, status.action);
|
|
261
290
|
service.sendStatus(
|
|
@@ -264,6 +293,7 @@ function sendStatusUpdate(service: KichiForwarderService, status: ActionResult):
|
|
|
264
293
|
status.bubble || status.action,
|
|
265
294
|
typeof status.log === "string" ? status.log.trim() : "",
|
|
266
295
|
getActionPlayback(actionDefinition),
|
|
296
|
+
status.avatarStatus,
|
|
267
297
|
status.propId,
|
|
268
298
|
);
|
|
269
299
|
}
|
|
@@ -643,6 +673,13 @@ function isIdlePlanPomodoroPhase(value: unknown): value is IdlePlanPomodoroPhase
|
|
|
643
673
|
return IDLE_PLAN_POMODORO_PHASES.includes(String(value) as IdlePlanPomodoroPhase);
|
|
644
674
|
}
|
|
645
675
|
|
|
676
|
+
function normalizeAvatarStatus(value: unknown, fieldPath: string): { avatarStatus?: AvatarStatus; error?: string } {
|
|
677
|
+
if (typeof value !== "string" || !AVATAR_STATUSES.includes(value as AvatarStatusName)) {
|
|
678
|
+
return { error: `${fieldPath} must be one of: ${AVATAR_STATUSES.join(", ")}` };
|
|
679
|
+
}
|
|
680
|
+
return { avatarStatus: value as AvatarStatus };
|
|
681
|
+
}
|
|
682
|
+
|
|
646
683
|
function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: string } {
|
|
647
684
|
if (!isPlainObject(value)) {
|
|
648
685
|
return { error: "idle plan payload must be an object" };
|
|
@@ -678,6 +715,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
678
715
|
const name = rawStage.name;
|
|
679
716
|
const purpose = rawStage.purpose;
|
|
680
717
|
const pomodoroPhase = rawStage.pomodoroPhase;
|
|
718
|
+
const avatarStatus = rawStage.avatarStatus;
|
|
681
719
|
const durationSeconds = rawStage.durationSeconds;
|
|
682
720
|
const actions = rawStage.actions;
|
|
683
721
|
|
|
@@ -692,6 +730,10 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
692
730
|
error: `stages[${stageIndex}].pomodoroPhase must be one of: ${IDLE_PLAN_POMODORO_PHASES.join(", ")}`,
|
|
693
731
|
};
|
|
694
732
|
}
|
|
733
|
+
const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, `stages[${stageIndex}].avatarStatus`);
|
|
734
|
+
if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
|
|
735
|
+
return { error: normalizedAvatarStatus.error ?? `stages[${stageIndex}].avatarStatus is invalid` };
|
|
736
|
+
}
|
|
695
737
|
if (!isPositiveInteger(durationSeconds)) {
|
|
696
738
|
return { error: `stages[${stageIndex}].durationSeconds must be a positive integer` };
|
|
697
739
|
}
|
|
@@ -776,6 +818,7 @@ function normalizeIdlePlan(value: unknown): { idlePlan?: IdlePlan; error?: strin
|
|
|
776
818
|
name: name.trim(),
|
|
777
819
|
purpose: purpose.trim(),
|
|
778
820
|
pomodoroPhase,
|
|
821
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
779
822
|
durationSeconds,
|
|
780
823
|
actions: normalizedActions,
|
|
781
824
|
});
|
|
@@ -1001,6 +1044,7 @@ function buildKichiActionDescription(service?: KichiForwarderService): string {
|
|
|
1001
1044
|
"Directly control the avatar inside Kichi World.",
|
|
1002
1045
|
"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.",
|
|
1003
1046
|
"For most work, prefer a sit pose and switch actions as the task moves between stages.",
|
|
1047
|
+
"Set avatarStatus to the current avatar status: Idle, Busy, Activities, or Break.",
|
|
1004
1048
|
"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.",
|
|
1005
1049
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
1006
1050
|
`sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
|
|
@@ -1032,9 +1076,10 @@ function buildKichiIdlePlanDescription(): string {
|
|
|
1032
1076
|
"2. Set the overall goal to that project. Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary as the goal.",
|
|
1033
1077
|
"3. Break the full heartbeat interval into ordered stages. Each stage purpose must explain what you are actually doing in that stage as part of the same project, not just how you want to feel. Do not switch to unrelated tasks just to use more actions.",
|
|
1034
1078
|
"4. Make the full stage duration total exactly to the heartbeat interval, and assign each stage pomodoroPhase from the stage's actual role: focus for concentrated activity, shortBreak for short resets, longBreak for longer rests. Do not default the whole idle plan to none. Use none only for a stage that truly has no pomodoro role.",
|
|
1035
|
-
"5.
|
|
1036
|
-
"6.
|
|
1037
|
-
"7.
|
|
1079
|
+
"5. Set each stage avatarStatus to the avatar status for that stage: Idle, Busy, Activities, or Break.",
|
|
1080
|
+
"6. Choose stage actions that clearly match the stage purpose and the project.",
|
|
1081
|
+
"7. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
|
|
1082
|
+
"8. If an idle plan is currently being carried out and the user asks about something from it, respond with an immersive in-universe depiction or draft instead of bluntly saying it is fake or nonexistent.",
|
|
1038
1083
|
"Use your memory to recall what you did in past heartbeats and to stay consistent with your established personality and interests.",
|
|
1039
1084
|
"Use the same language as the current conversation for goal, purpose, bubble, and log.",
|
|
1040
1085
|
`stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
|
|
@@ -1059,6 +1104,7 @@ function buildKichiPrompt(): string {
|
|
|
1059
1104
|
"2. Step switch: call when the task moves into a different stage. Keep the pose aligned with the work, usually staying seated while switching actions within the task as needed.",
|
|
1060
1105
|
"3. Task end: call BEFORE final reply. Use the order `kichi_action` -> reply.",
|
|
1061
1106
|
"bubble: 2-5 word companion speech. log: one short natural first-person sentence under 15 words. Match the language of the bubble and mention the current action and immediate focus like a real companion.",
|
|
1107
|
+
"avatarStatus: set the current avatar status as Idle, Busy, Activities, or Break.",
|
|
1062
1108
|
"",
|
|
1063
1109
|
"kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
|
|
1064
1110
|
"",
|
|
@@ -1166,18 +1212,29 @@ const plugin = {
|
|
|
1166
1212
|
api.registerTool((ctx) => ({
|
|
1167
1213
|
name: "kichi_join",
|
|
1168
1214
|
label: "kichi_join",
|
|
1169
|
-
description:
|
|
1215
|
+
description:
|
|
1216
|
+
"Join Kichi world in the target environment with avatarId, the current bot name, a short bio, and personality tags. For test, pass host.",
|
|
1170
1217
|
parameters: {
|
|
1171
1218
|
type: "object",
|
|
1172
1219
|
properties: {
|
|
1173
1220
|
avatarId: { type: "string", description: "Avatar ID to join Kichi world" },
|
|
1221
|
+
environment: {
|
|
1222
|
+
type: "string",
|
|
1223
|
+
enum: VALID_ENVIRONMENTS,
|
|
1224
|
+
description:
|
|
1225
|
+
"Target environment. kichi_join switches to this environment before joining.",
|
|
1226
|
+
},
|
|
1227
|
+
host: {
|
|
1228
|
+
type: "string",
|
|
1229
|
+
description: "Test host, required when environment is test and ignored otherwise",
|
|
1230
|
+
},
|
|
1174
1231
|
botName: {
|
|
1175
1232
|
type: "string",
|
|
1176
1233
|
description: "Current bot name to include in the join message",
|
|
1177
1234
|
},
|
|
1178
1235
|
bio: {
|
|
1179
1236
|
type: "string",
|
|
1180
|
-
description: "Short bio covering
|
|
1237
|
+
description: "Short bio extracted from SOUL.md, covering persona and idle plan goals if present",
|
|
1181
1238
|
},
|
|
1182
1239
|
tags: {
|
|
1183
1240
|
type: "array",
|
|
@@ -1189,7 +1246,7 @@ const plugin = {
|
|
|
1189
1246
|
description: "Optional join source identifier. Defaults to Kichi World join-source.json, then openclaw.",
|
|
1190
1247
|
},
|
|
1191
1248
|
},
|
|
1192
|
-
required: ["botName", "bio"],
|
|
1249
|
+
required: ["environment", "avatarId", "botName", "bio"],
|
|
1193
1250
|
},
|
|
1194
1251
|
execute: async (_toolCallId, params) => {
|
|
1195
1252
|
const locator = resolveToolLocator(ctx);
|
|
@@ -1198,19 +1255,33 @@ const plugin = {
|
|
|
1198
1255
|
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1199
1256
|
}
|
|
1200
1257
|
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1258
|
+
const p = params as {
|
|
1259
|
+
avatarId?: string;
|
|
1260
|
+
environment?: unknown;
|
|
1261
|
+
host?: unknown;
|
|
1262
|
+
botName?: string;
|
|
1263
|
+
bio?: string;
|
|
1264
|
+
source?: unknown;
|
|
1265
|
+
tags?: unknown;
|
|
1266
|
+
} | null;
|
|
1267
|
+
const target = resolveJoinEnvironmentHost({
|
|
1268
|
+
environment: p?.environment,
|
|
1269
|
+
host: p?.host,
|
|
1270
|
+
});
|
|
1271
|
+
if (target.error) {
|
|
1272
|
+
return jsonResult({ success: false, error: target.error });
|
|
1210
1273
|
}
|
|
1211
|
-
|
|
1212
|
-
|
|
1274
|
+
const currentStatus = service.getConnectionStatus();
|
|
1275
|
+
let avatarId = p?.avatarId;
|
|
1276
|
+
if (!avatarId && currentStatus.host === target.host) {
|
|
1277
|
+
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
1213
1278
|
}
|
|
1279
|
+
const botName = p?.botName?.trim();
|
|
1280
|
+
const bio = p?.bio?.trim();
|
|
1281
|
+
const rawSource = p?.source;
|
|
1282
|
+
const { tags, error: tagsError } = normalizeJoinTags(
|
|
1283
|
+
p?.tags,
|
|
1284
|
+
);
|
|
1214
1285
|
if (!botName) {
|
|
1215
1286
|
return jsonResult({ success: false, error: "No botName" });
|
|
1216
1287
|
}
|
|
@@ -1231,14 +1302,50 @@ const plugin = {
|
|
|
1231
1302
|
if (tagsError) {
|
|
1232
1303
|
return jsonResult({ success: false, error: tagsError });
|
|
1233
1304
|
}
|
|
1305
|
+
let leaveStatus;
|
|
1306
|
+
const shouldLeaveCurrentConnection = currentStatus.connected && currentStatus.hasAuthKey && (
|
|
1307
|
+
(!!currentStatus.host && currentStatus.host !== target.host) ||
|
|
1308
|
+
(currentStatus.host === target.host && !!currentStatus.avatarId && !!avatarId && currentStatus.avatarId !== avatarId)
|
|
1309
|
+
);
|
|
1310
|
+
if (shouldLeaveCurrentConnection) {
|
|
1311
|
+
try {
|
|
1312
|
+
leaveStatus = await service.leave();
|
|
1313
|
+
} catch (err) {
|
|
1314
|
+
leaveStatus = {
|
|
1315
|
+
success: false,
|
|
1316
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1317
|
+
};
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
let switchStatus;
|
|
1321
|
+
if (target.environment && target.host && service.getCurrentHost() !== target.host) {
|
|
1322
|
+
switchStatus = await service.switchHost(target.host, target.environment);
|
|
1323
|
+
}
|
|
1324
|
+
if (!avatarId) {
|
|
1325
|
+
avatarId = service.readSavedAvatarId() ?? undefined;
|
|
1326
|
+
}
|
|
1327
|
+
if (!avatarId) {
|
|
1328
|
+
return jsonResult({ success: false, error: "No avatarId" });
|
|
1329
|
+
}
|
|
1234
1330
|
const result = await service.join(avatarId, botName, bio, tags ?? [], source);
|
|
1235
1331
|
if (result.success) {
|
|
1236
|
-
return jsonResult({
|
|
1332
|
+
return jsonResult({
|
|
1333
|
+
success: true,
|
|
1334
|
+
authKey: result.authKey,
|
|
1335
|
+
...(target.environment ? { environment: target.environment } : {}),
|
|
1336
|
+
...(target.host ? { host: target.host } : {}),
|
|
1337
|
+
...(switchStatus ? { switchStatus } : {}),
|
|
1338
|
+
...(leaveStatus ? { leaveStatus } : {}),
|
|
1339
|
+
});
|
|
1237
1340
|
}
|
|
1238
1341
|
const failure = result as { success: false; error: string; errorCode?: string; errorMessage?: string };
|
|
1239
1342
|
return jsonResult({
|
|
1240
1343
|
success: false,
|
|
1241
1344
|
error: failure.error,
|
|
1345
|
+
...(target.environment ? { environment: target.environment } : {}),
|
|
1346
|
+
...(target.host ? { host: target.host } : {}),
|
|
1347
|
+
...(switchStatus ? { switchStatus } : {}),
|
|
1348
|
+
...(leaveStatus ? { leaveStatus } : {}),
|
|
1242
1349
|
...(failure.errorCode ? { errorCode: failure.errorCode } : {}),
|
|
1243
1350
|
...(failure.errorMessage ? { errorMessage: failure.errorMessage } : {}),
|
|
1244
1351
|
});
|
|
@@ -1260,7 +1367,7 @@ const plugin = {
|
|
|
1260
1367
|
},
|
|
1261
1368
|
host: {
|
|
1262
1369
|
type: "string",
|
|
1263
|
-
description: "Test
|
|
1370
|
+
description: "Test host (required for test environment, ignored otherwise)",
|
|
1264
1371
|
},
|
|
1265
1372
|
},
|
|
1266
1373
|
required: ["environment"],
|
|
@@ -1386,6 +1493,11 @@ const plugin = {
|
|
|
1386
1493
|
description: "Action name for the selected pose (for example Sit Nicely, Typing with Keyboard, Reading, High Five, or Meditate)",
|
|
1387
1494
|
},
|
|
1388
1495
|
bubble: { type: "string", description: "Optional bubble text to display (max 5 words)" },
|
|
1496
|
+
avatarStatus: {
|
|
1497
|
+
type: "string",
|
|
1498
|
+
description: "Current avatar status: Idle, Busy, Activities, or Break.",
|
|
1499
|
+
enum: [...AVATAR_STATUSES],
|
|
1500
|
+
},
|
|
1389
1501
|
log: {
|
|
1390
1502
|
type: "string",
|
|
1391
1503
|
description:
|
|
@@ -1402,7 +1514,7 @@ const plugin = {
|
|
|
1402
1514
|
"Optional poseable prop ID from RoomContext.PoseableProps (obtained via kichi_query_status or cached). When specified, the avatar is seated at this prop; when omitted, the server picks the nearest available prop.",
|
|
1403
1515
|
},
|
|
1404
1516
|
},
|
|
1405
|
-
required: ["poseType", "action"],
|
|
1517
|
+
required: ["poseType", "action", "avatarStatus"],
|
|
1406
1518
|
},
|
|
1407
1519
|
execute: async (_toolCallId, params) => {
|
|
1408
1520
|
const locator = resolveToolLocator(ctx);
|
|
@@ -1411,10 +1523,11 @@ const plugin = {
|
|
|
1411
1523
|
return jsonResult({ success: false, error: "Failed to resolve agent-scoped Kichi runtime" });
|
|
1412
1524
|
}
|
|
1413
1525
|
const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
|
|
1414
|
-
const { poseType, action, bubble, log, verify, propId } = (params || {}) as {
|
|
1526
|
+
const { poseType, action, bubble, avatarStatus, log, verify, propId } = (params || {}) as {
|
|
1415
1527
|
poseType?: string;
|
|
1416
1528
|
action?: string;
|
|
1417
1529
|
bubble?: string;
|
|
1530
|
+
avatarStatus?: unknown;
|
|
1418
1531
|
log?: string;
|
|
1419
1532
|
verify?: boolean;
|
|
1420
1533
|
propId?: string;
|
|
@@ -1428,6 +1541,10 @@ const plugin = {
|
|
|
1428
1541
|
error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
|
|
1429
1542
|
});
|
|
1430
1543
|
}
|
|
1544
|
+
const normalizedAvatarStatus = normalizeAvatarStatus(avatarStatus, "avatarStatus");
|
|
1545
|
+
if (normalizedAvatarStatus.error || normalizedAvatarStatus.avatarStatus === undefined) {
|
|
1546
|
+
return jsonResult({ success: false, error: normalizedAvatarStatus.error ?? "avatarStatus is invalid" });
|
|
1547
|
+
}
|
|
1431
1548
|
if (!service.hasValidIdentity() || !service.isConnected()) {
|
|
1432
1549
|
return jsonResult({ success: false, error: "Not connected to Kichi world" });
|
|
1433
1550
|
}
|
|
@@ -1450,7 +1567,13 @@ const plugin = {
|
|
|
1450
1567
|
if (verify) {
|
|
1451
1568
|
try {
|
|
1452
1569
|
const ack = await service.sendStatusVerified(
|
|
1453
|
-
normalizedPoseType,
|
|
1570
|
+
normalizedPoseType,
|
|
1571
|
+
matched.name,
|
|
1572
|
+
bubbleText,
|
|
1573
|
+
logText,
|
|
1574
|
+
playback,
|
|
1575
|
+
normalizedAvatarStatus.avatarStatus,
|
|
1576
|
+
propId,
|
|
1454
1577
|
);
|
|
1455
1578
|
if (ack.warning) {
|
|
1456
1579
|
return jsonResult({
|
|
@@ -1469,6 +1592,7 @@ const plugin = {
|
|
|
1469
1592
|
action: matched.name,
|
|
1470
1593
|
bubble: bubbleText,
|
|
1471
1594
|
log: logText,
|
|
1595
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
1472
1596
|
propId,
|
|
1473
1597
|
});
|
|
1474
1598
|
}
|
|
@@ -1479,6 +1603,7 @@ const plugin = {
|
|
|
1479
1603
|
action: matched.name,
|
|
1480
1604
|
bubble: bubbleText,
|
|
1481
1605
|
log: logText,
|
|
1606
|
+
avatarStatus: normalizedAvatarStatus.avatarStatus,
|
|
1482
1607
|
playback,
|
|
1483
1608
|
});
|
|
1484
1609
|
},
|
|
@@ -1586,6 +1711,11 @@ const plugin = {
|
|
|
1586
1711
|
description: "Pomodoro phase for this stage: focus, shortBreak, longBreak, or none. Set it from the stage's actual role. Treat none as exceptional, not the default for the whole plan.",
|
|
1587
1712
|
enum: [...IDLE_PLAN_POMODORO_PHASES],
|
|
1588
1713
|
},
|
|
1714
|
+
avatarStatus: {
|
|
1715
|
+
type: "string",
|
|
1716
|
+
description: "Avatar status for this stage: Idle, Busy, Activities, or Break.",
|
|
1717
|
+
enum: [...AVATAR_STATUSES],
|
|
1718
|
+
},
|
|
1589
1719
|
durationSeconds: {
|
|
1590
1720
|
type: "number",
|
|
1591
1721
|
description: "Required duration in seconds for this stage.",
|
|
@@ -1625,7 +1755,7 @@ const plugin = {
|
|
|
1625
1755
|
},
|
|
1626
1756
|
},
|
|
1627
1757
|
},
|
|
1628
|
-
required: ["name", "purpose", "pomodoroPhase", "durationSeconds", "actions"],
|
|
1758
|
+
required: ["name", "purpose", "pomodoroPhase", "avatarStatus", "durationSeconds", "actions"],
|
|
1629
1759
|
},
|
|
1630
1760
|
},
|
|
1631
1761
|
},
|
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.19",
|
|
6
6
|
"author": "OpenClaw",
|
|
7
7
|
"skills": ["./skills/kichi-forwarder"],
|
|
8
8
|
"contracts": {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@yahaha-studio/kichi-forwarder",
|
|
3
|
-
"version": "0.1.2-beta.
|
|
3
|
+
"version": "0.1.2-beta.19",
|
|
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": "./dist/index.js",
|
|
@@ -23,7 +23,7 @@ Install and connect requests use `on <environment>` syntax. Supported environmen
|
|
|
23
23
|
|
|
24
24
|
- `steam`: connects to `focus-wss.yahaha.com`
|
|
25
25
|
- `steam-playtest`: connects to `focus-steam-playtest-wss-int.yahaha.com`
|
|
26
|
-
- `test`: no fixed host —
|
|
26
|
+
- `test`: no fixed host — use the test host from the user request when provided; otherwise ask the user for the test host before calling `kichi_join`
|
|
27
27
|
|
|
28
28
|
## Runtime State
|
|
29
29
|
|
|
@@ -69,26 +69,23 @@ For install/onboarding/connect requests:
|
|
|
69
69
|
|
|
70
70
|
Use this order unless the user asks for a different explicit action. For install/onboarding requests, follow `install.md` first.
|
|
71
71
|
|
|
72
|
-
1.
|
|
73
|
-
2. If the requested environment differs from the current environment, call `kichi_switch_host` with the target environment.
|
|
74
|
-
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`.
|
|
75
|
-
4. If no `authKey` is available, call `kichi_join`.
|
|
76
|
-
5. If `authKey` exists but websocket is not open, call `kichi_rejoin` or wait for automatic reconnect and rejoin.
|
|
77
|
-
6. Use `kichi_action`, `kichi_glance`, `kichi_clock`, note board tools, and music album tools after status is ready.
|
|
72
|
+
1. For join/connect requests with an `avatarId` and environment, call `kichi_join` with `environment`. For `test`, include `host` if the user provided it; if not, ask for the host first.
|
|
78
73
|
|
|
79
74
|
## Tools
|
|
80
75
|
|
|
81
76
|
### kichi_join
|
|
82
77
|
|
|
83
78
|
```text
|
|
84
|
-
kichi_join(avatarId: "your-avatar-id", botName: "<from IDENTITY.md>", bio: "<from SOUL.md>", tags: ["calm", "focused", "curious"])
|
|
79
|
+
kichi_join(environment: "steam-playtest", avatarId: "your-avatar-id", botName: "<from IDENTITY.md>", bio: "<from SOUL.md>", tags: ["calm", "focused", "curious"])
|
|
80
|
+
kichi_join(environment: "test", host: "192.168.1.100", avatarId: "your-avatar-id", botName: "<from IDENTITY.md>", bio: "<from SOUL.md>", tags: ["calm", "focused", "curious"])
|
|
85
81
|
```
|
|
86
82
|
|
|
83
|
+
- `environment`: required. One of `steam`, `steam-playtest`, `test`. `kichi_join` switches to the target environment before joining.
|
|
84
|
+
- `host`: required for `test` environment, ignored otherwise. If the user did not provide the test host, ask for it before calling `kichi_join`.
|
|
85
|
+
- `avatarId`: required
|
|
87
86
|
- `botName`: required
|
|
88
|
-
- `bio`: required
|
|
89
|
-
- `avatarId`: optional. If omitted, the tool reads `avatarId` from the current host's `identity.json`. If missing, the call fails.
|
|
87
|
+
- `bio`: required. Extract from `SOUL.md`, covering persona and idle plan goals if present.
|
|
90
88
|
- `tags`: optional string list. Empty strings are ignored and duplicates are removed. If omitted, the join payload sends `[]`.
|
|
91
|
-
- If the current host is still joined with a different `avatarId`, call `kichi_leave` first, then call `kichi_join` with the new `avatarId`.
|
|
92
89
|
|
|
93
90
|
### kichi_switch_host
|
|
94
91
|
|
|
@@ -98,7 +95,7 @@ kichi_switch_host(environment: "test", host: "192.168.1.100")
|
|
|
98
95
|
```
|
|
99
96
|
|
|
100
97
|
- `environment`: required. One of `steam`, `steam-playtest`, `test`.
|
|
101
|
-
- `host`: required for `test` environment, ignored otherwise.
|
|
98
|
+
- `host`: required for `test` environment, ignored otherwise.
|
|
102
99
|
- For `steam` and `steam-playtest`, the host is resolved automatically from the bundled config.
|
|
103
100
|
- This reloads the host-specific `identity.json` and reconnects the websocket immediately.
|
|
104
101
|
|
|
@@ -6,7 +6,7 @@ Install and connect requests use `on <environment>` syntax. Supported environmen
|
|
|
6
6
|
|
|
7
7
|
- `steam`: connects to `focus-wss.yahaha.com`
|
|
8
8
|
- `steam-playtest`: connects to `focus-steam-playtest-wss-int.yahaha.com`
|
|
9
|
-
- `test`: no fixed host —
|
|
9
|
+
- `test`: no fixed host — use the test host from the user request when provided; otherwise ask the user for the test host before calling `kichi_join`
|
|
10
10
|
|
|
11
11
|
## Runtime Files
|
|
12
12
|
|
|
@@ -23,17 +23,6 @@ Persist runtime state to the current agent's `state.json`:
|
|
|
23
23
|
}
|
|
24
24
|
```
|
|
25
25
|
|
|
26
|
-
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):
|
|
27
|
-
|
|
28
|
-
- Linux/macOS: `~/.openclaw/kichi-world/agents/<encoded-agent-id>/hosts/<encoded-host>/identity.json`
|
|
29
|
-
- Windows: `%USERPROFILE%\.openclaw\kichi-world\agents\<encoded-agent-id>\hosts\<encoded-host>\identity.json`
|
|
30
|
-
|
|
31
|
-
```json
|
|
32
|
-
{
|
|
33
|
-
"avatarId": "your-avatar-id"
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
26
|
## Install
|
|
38
27
|
|
|
39
28
|
1. Download the npm package archive:
|
|
@@ -64,7 +53,7 @@ When the user asks with one of the commands above, execute in this fixed order:
|
|
|
64
53
|
|
|
65
54
|
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.
|
|
66
55
|
2. Parse `avatarId` from user text (`AvatarId`/`avatarId`, case-insensitive).
|
|
67
|
-
3. Parse environment from the `on <environment>` part of the command (e.g. `on steam-playtest`).
|
|
56
|
+
3. Parse environment from the `on <environment>` part of the command (e.g. `on steam-playtest`). For `test`, parse the host from the request or ask the user for it before continuing.
|
|
68
57
|
4. Run `npm pack @yahaha-studio/kichi-forwarder`, then install the generated `.tgz` with `openclaw plugins install <tgz-path>`.
|
|
69
58
|
5. If the plugin already exists and the packed version matches the installed version, skip to step 7.
|
|
70
59
|
6. If the plugin already exists but the version differs, overwrite with `openclaw plugins install <tgz-path> --force`.
|
|
@@ -72,11 +61,7 @@ When the user asks with one of the commands above, execute in this fixed order:
|
|
|
72
61
|
8. Run `openclaw --version`. If the version is **5.7 or later**, ensure `openclaw.json` has `plugins.entries.kichi-forwarder.hooks.allowConversationAccess` set to `true`. If missing, add it. On older versions, skip this step.
|
|
73
62
|
9. If the plugin was newly installed or upgraded in this flow, check workspace `HEARTBEAT.md` against the latest Kichi heartbeat requirements before continuing. An empty or blank `HEARTBEAT.md` means the snippet is missing — treat it the same as "snippet not found", not as a read failure.
|
|
74
63
|
10. 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.
|
|
75
|
-
11. Call `
|
|
76
|
-
12. If the current agent runtime environment does not match the requested one, call `kichi_switch_host` with the target environment (and host for test).
|
|
77
|
-
13. 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`.
|
|
78
|
-
14. Otherwise, if `authKey` is missing, call `kichi_join` with parsed `avatarId`, `botName`, `bio`, and `tags`.
|
|
79
|
-
15. Call `kichi_connection_status` again and confirm connection and auth state.
|
|
64
|
+
11. Call `kichi_join` with parsed `environment`, `host` for test, `avatarId`, `botName`, `bio`, and `tags`.
|
|
80
65
|
|
|
81
66
|
## Required Post-install Integration
|
|
82
67
|
|
|
@@ -85,7 +70,7 @@ Use this completion checklist:
|
|
|
85
70
|
- [ ] plugin installed, enabled, and at latest version
|
|
86
71
|
- [ ] `openclaw.json` has `plugins.entries.kichi-forwarder.hooks.allowConversationAccess: true` (OpenClaw >= 5.7 only)
|
|
87
72
|
- [ ] `HEARTBEAT.md` updated with the Kichi heartbeat workflow snippet from [heartbeat.md](heartbeat.md)
|
|
88
|
-
- [ ] `
|
|
73
|
+
- [ ] `kichi_join` completed successfully
|
|
89
74
|
|
|
90
75
|
If any box is unchecked, the onboarding remains incomplete.
|
|
91
76
|
|
package/src/service.ts
CHANGED
|
@@ -5,6 +5,7 @@ import { randomUUID } from "node:crypto";
|
|
|
5
5
|
import type { PluginLogger } from "openclaw/plugin-sdk";
|
|
6
6
|
import type {
|
|
7
7
|
ActionPlayback,
|
|
8
|
+
AvatarStatus,
|
|
8
9
|
BotMessageHistoryEntry,
|
|
9
10
|
BotMessagePayload,
|
|
10
11
|
BotMessageReceivedPayload,
|
|
@@ -189,7 +190,15 @@ export class KichiForwarderService {
|
|
|
189
190
|
});
|
|
190
191
|
}
|
|
191
192
|
|
|
192
|
-
sendStatus(
|
|
193
|
+
sendStatus(
|
|
194
|
+
poseType: PoseType | "",
|
|
195
|
+
action: string,
|
|
196
|
+
bubble: string,
|
|
197
|
+
log: string,
|
|
198
|
+
playback: ActionPlayback,
|
|
199
|
+
avatarStatus: AvatarStatus,
|
|
200
|
+
propId?: string,
|
|
201
|
+
): void {
|
|
193
202
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
194
203
|
const payload: StatusPayload = {
|
|
195
204
|
type: "status",
|
|
@@ -200,6 +209,7 @@ export class KichiForwarderService {
|
|
|
200
209
|
bubble,
|
|
201
210
|
log,
|
|
202
211
|
playback,
|
|
212
|
+
avatarStatus,
|
|
203
213
|
...(propId ? { propId } : {}),
|
|
204
214
|
};
|
|
205
215
|
this.ws.send(JSON.stringify(payload));
|
|
@@ -211,6 +221,7 @@ export class KichiForwarderService {
|
|
|
211
221
|
bubble: string,
|
|
212
222
|
log: string,
|
|
213
223
|
playback: ActionPlayback,
|
|
224
|
+
avatarStatus: AvatarStatus,
|
|
214
225
|
propId?: string,
|
|
215
226
|
): Promise<StatusAckPayload> {
|
|
216
227
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) {
|
|
@@ -226,6 +237,7 @@ export class KichiForwarderService {
|
|
|
226
237
|
bubble,
|
|
227
238
|
log,
|
|
228
239
|
playback,
|
|
240
|
+
avatarStatus,
|
|
229
241
|
...(propId ? { propId } : {}),
|
|
230
242
|
};
|
|
231
243
|
return this.sendRequest<StatusAckPayload>(payload, "status_ack", 5000);
|
package/src/types.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type KichiForwarderConfig = Record<string, never>;
|
|
|
2
2
|
|
|
3
3
|
export type PoseType = "stand" | "sit" | "lay" | "floor";
|
|
4
4
|
export type ActionPlaybackMode = "loop" | "once";
|
|
5
|
+
export type AvatarStatus = "Idle" | "Busy" | "Activities" | "Break";
|
|
5
6
|
export type ActionPlayback = {
|
|
6
7
|
mode: ActionPlaybackMode;
|
|
7
8
|
resumeAction?: string;
|
|
@@ -16,6 +17,7 @@ export type ActionResult = {
|
|
|
16
17
|
poseType: PoseType;
|
|
17
18
|
action: string;
|
|
18
19
|
bubble: string;
|
|
20
|
+
avatarStatus: AvatarStatus;
|
|
19
21
|
log?: string;
|
|
20
22
|
propId?: string;
|
|
21
23
|
};
|
|
@@ -120,6 +122,7 @@ export type StatusPayload = {
|
|
|
120
122
|
bubble: string;
|
|
121
123
|
log: string;
|
|
122
124
|
playback: ActionPlayback;
|
|
125
|
+
avatarStatus: AvatarStatus;
|
|
123
126
|
propId?: string;
|
|
124
127
|
};
|
|
125
128
|
|
|
@@ -173,6 +176,7 @@ export type IdlePlanStage = {
|
|
|
173
176
|
name: string;
|
|
174
177
|
purpose: string;
|
|
175
178
|
pomodoroPhase: IdlePlanPhase;
|
|
179
|
+
avatarStatus: AvatarStatus;
|
|
176
180
|
durationSeconds: number;
|
|
177
181
|
actions: IdlePlanStageAction[];
|
|
178
182
|
};
|