@yahaha-studio/kichi-forwarder 0.1.0-beta.8 → 0.1.1-beta.1

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 CHANGED
@@ -50,15 +50,15 @@ Get the `host` and `avatarId` from Kichi, then use them with `kichi_switch_host`
50
50
 
51
51
  ## Runtime State
52
52
 
53
- The plugin stores runtime state in the OpenClaw user directory:
53
+ The plugin stores runtime state per OpenClaw agent in the OpenClaw user directory:
54
54
 
55
- - Windows: `%USERPROFILE%\.openclaw\kichi-world\`
56
- - Linux/macOS: `~/.openclaw/kichi-world/`
55
+ - Windows: `%USERPROFILE%\.openclaw\kichi-world\agents\<encoded-agent-id>\`
56
+ - Linux/macOS: `~/.openclaw/kichi-world/agents/<encoded-agent-id>/`
57
57
 
58
- Important files:
58
+ Important files for each agent:
59
59
 
60
- - `state.json` stores the current host and `llmRuntimeEnabled`
61
- - `hosts/<encoded-host>/identity.json` stores host-specific `avatarId` and `authKey`
60
+ - `state.json` stores that agent's current host and `llmRuntimeEnabled`
61
+ - `hosts/<encoded-host>/identity.json` stores that agent's host-specific `avatarId` and `authKey`
62
62
 
63
63
  ## Notes
64
64
 
@@ -140,10 +140,6 @@
140
140
  "playback": "once",
141
141
  "resumeAction": "Idle Backup Hands"
142
142
  },
143
- {
144
- "name": "Walk",
145
- "playback": "loop"
146
- },
147
143
  {
148
144
  "name": "Goofy Moves",
149
145
  "playback": "loop"
package/index.ts CHANGED
@@ -1,9 +1,12 @@
1
- import fs from "node:fs";
2
- import os from "node:os";
3
- import path from "node:path";
1
+ import fs from "node:fs";
4
2
  import { fileURLToPath } from "node:url";
5
- import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
3
+ import type {
4
+ AnyAgentTool,
5
+ OpenClawPluginApi,
6
+ OpenClawPluginToolContext,
7
+ } from "openclaw/plugin-sdk";
6
8
  import { parse } from "./src/config.js";
9
+ import { KichiRuntimeManager } from "./src/runtime-manager.js";
7
10
  import { KichiForwarderService } from "./src/service.js";
8
11
  import type {
9
12
  ActionDefinition,
@@ -12,13 +15,11 @@ import type {
12
15
  Album,
13
16
  ClockAction,
14
17
  ClockConfig,
15
- KichiState,
16
18
  KichiStaticConfig,
17
19
  PomodoroPhase,
18
20
  PoseType,
19
21
  } from "./src/types.js";
20
22
  const BUNDLED_STATIC_CONFIG_PATH = new URL("./config/kichi-config.json", import.meta.url);
21
- const DEFAULT_LLM_RUNTIME_ENABLED = true;
22
23
  const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
23
24
  beforePromptBuild: {
24
25
  poseType: "sit",
@@ -46,8 +47,6 @@ const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
46
47
  },
47
48
  };
48
49
 
49
- const KICHI_WORLD_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
50
- const STATE_PATH = path.join(KICHI_WORLD_DIR, "state.json");
51
50
  const MAX_NOTEBOARD_TEXT_LENGTH = 200;
52
51
  const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
53
52
  const MAX_AGENT_END_PREVIEW_WIDTH = 10;
@@ -55,8 +54,6 @@ const MESSAGE_RECEIVED_ELLIPSIS = "...";
55
54
  const IDLE_PLAN_POMODORO_PHASES = ["focus", "shortBreak", "longBreak", "none"] as const;
56
55
  let cachedStaticConfig: KichiStaticConfig | null = null;
57
56
  let cachedStaticConfigMtime = 0;
58
- let service: KichiForwarderService | null = null;
59
- let pluginApi: OpenClawPluginApi | null = null;
60
57
 
61
58
  type IdlePlanPomodoroPhase = typeof IDLE_PLAN_POMODORO_PHASES[number];
62
59
  type IdlePlanAction = {
@@ -192,25 +189,6 @@ function normalizeStaticConfig(value: unknown): KichiStaticConfig {
192
189
  };
193
190
  }
194
191
 
195
- function readState(): KichiState {
196
- if (!fs.existsSync(STATE_PATH)) {
197
- return {
198
- llmRuntimeEnabled: DEFAULT_LLM_RUNTIME_ENABLED,
199
- };
200
- }
201
- const data = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8")) as Partial<KichiState>;
202
- if (data.currentHost !== undefined && (typeof data.currentHost !== "string" || !data.currentHost.trim())) {
203
- throw new Error(`Invalid currentHost in ${STATE_PATH}`);
204
- }
205
- if (typeof data.llmRuntimeEnabled !== "boolean") {
206
- throw new Error(`Invalid llmRuntimeEnabled in ${STATE_PATH}`);
207
- }
208
- return {
209
- ...(typeof data.currentHost === "string" ? { currentHost: data.currentHost } : {}),
210
- llmRuntimeEnabled: data.llmRuntimeEnabled,
211
- };
212
- }
213
-
214
192
  function loadStaticConfig(): KichiStaticConfig {
215
193
  const configPath = fileURLToPath(BUNDLED_STATIC_CONFIG_PATH);
216
194
  const stat = fs.statSync(configPath);
@@ -222,9 +200,9 @@ function loadStaticConfig(): KichiStaticConfig {
222
200
  return cachedStaticConfig;
223
201
  }
224
202
 
225
- function sendStatusUpdate(status: ActionResult): void {
203
+ function sendStatusUpdate(service: KichiForwarderService, status: ActionResult): void {
226
204
  const actionDefinition = getActionDefinition(status.poseType, status.action);
227
- service?.sendStatus(
205
+ service.sendStatus(
228
206
  status.poseType,
229
207
  actionDefinition.name,
230
208
  status.bubble || status.action,
@@ -233,19 +211,15 @@ function sendStatusUpdate(status: ActionResult): void {
233
211
  );
234
212
  }
235
213
 
236
- function isLlmRuntimeEnabled(): boolean {
237
- return readState().llmRuntimeEnabled;
238
- }
239
-
240
- function syncFixedStatus(status: ActionResult): void {
241
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
214
+ function syncFixedStatus(service: KichiForwarderService, status: ActionResult): void {
215
+ if (!service.hasValidIdentity() || !service.isConnected()) {
242
216
  return;
243
217
  }
244
218
  const bubbleText = status.bubble.trim() || status.action;
245
219
  const logText = typeof status.log === "string" && status.log.trim()
246
220
  ? status.log.trim()
247
221
  : bubbleText;
248
- sendStatusUpdate({
222
+ sendStatusUpdate(service, {
249
223
  ...status,
250
224
  bubble: bubbleText,
251
225
  log: logText,
@@ -304,6 +278,56 @@ function stripReplyTag(text: string): string {
304
278
  return text.replace(/^\[\[\s*reply_to(?::[^\]]+|_current)?\s*\]\]\s*/i, "").trim();
305
279
  }
306
280
 
281
+ function stripKnownLeadingIdentifiers(text: string, candidates: string[]): string {
282
+ let normalized = text.trim();
283
+ if (!normalized) {
284
+ return "";
285
+ }
286
+
287
+ const separatorsPattern = String.raw`(?:[\s,:;,:;]|$)+`;
288
+ let changed = true;
289
+ while (changed && normalized) {
290
+ changed = false;
291
+ for (const candidate of candidates) {
292
+ const trimmed = candidate.trim();
293
+ if (!trimmed) {
294
+ continue;
295
+ }
296
+ const escaped = trimmed.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
297
+ const patterns = [
298
+ new RegExp(`^${escaped}${separatorsPattern}`, "i"),
299
+ new RegExp(`^@${escaped}${separatorsPattern}`, "i"),
300
+ new RegExp(`^<@${escaped}>${separatorsPattern}`, "i"),
301
+ ];
302
+ for (const pattern of patterns) {
303
+ if (!pattern.test(normalized)) {
304
+ continue;
305
+ }
306
+ normalized = normalized.replace(pattern, "").trimStart();
307
+ changed = true;
308
+ }
309
+ }
310
+ }
311
+
312
+ return normalized.trim();
313
+ }
314
+
315
+ function stripDispatchMetadata(
316
+ text: string,
317
+ context?: {
318
+ senderId?: string;
319
+ accountId?: string;
320
+ },
321
+ ): string {
322
+ let normalized = stripReplyTag(text);
323
+ normalized = normalized.replace(/^(?:\[[a-z_]+:\s*[^\]]+\]\s*)+/i, "").trim();
324
+ normalized = stripKnownLeadingIdentifiers(normalized, [
325
+ typeof context?.senderId === "string" ? context.senderId : "",
326
+ typeof context?.accountId === "string" ? context.accountId : "",
327
+ ]);
328
+ return normalized;
329
+ }
330
+
307
331
  function extractTextFromContent(content: unknown): string {
308
332
  if (typeof content === "string") {
309
333
  return stripReplyTag(content);
@@ -354,26 +378,65 @@ function getLastAssistantPreview(messages: unknown, maxWidth: number): string {
354
378
  return "";
355
379
  }
356
380
 
357
- async function handleMessageReceivedHook(content: string): Promise<void> {
358
- const connected = service?.isConnected() ?? false;
359
- const hasIdentity = service?.hasValidIdentity() ?? false;
360
- pluginApi?.logger.info(`[kichi] message_received hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
381
+ function resolveDispatchMessageText(
382
+ event: { body?: string; content: string },
383
+ context?: {
384
+ senderId?: string;
385
+ accountId?: string;
386
+ },
387
+ ): string {
388
+ if (typeof event.content === "string" && event.content.trim()) {
389
+ return stripDispatchMetadata(event.content, context);
390
+ }
391
+ if (typeof event.body === "string" && event.body.trim()) {
392
+ return stripDispatchMetadata(event.body, context);
393
+ }
394
+ return "";
395
+ }
396
+
397
+ function notifyMessageReceived(
398
+ api: OpenClawPluginApi,
399
+ service: KichiForwarderService,
400
+ content: string,
401
+ ): void {
402
+ const connected = service.isConnected();
403
+ const hasIdentity = service.hasValidIdentity();
404
+ api.logger.debug(`[kichi:${service.getAgentId()}] inbound sync fired (connected=${connected}, hasIdentity=${hasIdentity})`);
361
405
  if (!hasIdentity || !connected) {
362
- pluginApi?.logger.warn("[kichi] skipped message_received notify because service is not ready");
406
+ api.logger.debug(`[kichi:${service.getAgentId()}] skipped inbound sync because runtime is not ready`);
363
407
  return;
364
408
  }
365
409
  const trimmed = truncateByDisplayWidth(content, MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH);
366
- pluginApi?.logger.info(`[kichi] sending message_received notify with preview: ${trimmed || "(empty)"}`);
410
+ api.logger.debug(`[kichi:${service.getAgentId()}] sending message_received notify with preview: ${trimmed || "(empty)"}`);
367
411
  service.sendHookNotify("message_received", `"${trimmed}"`);
368
412
  }
369
413
 
370
- function registerPluginHooks(api: OpenClawPluginApi): void {
371
- api.on("before_prompt_build", () => {
372
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
414
+ function registerPluginHooks(api: OpenClawPluginApi, runtimeManager: KichiRuntimeManager): void {
415
+ api.on("before_dispatch", (event, ctx) => {
416
+ const service = runtimeManager.getRuntime(ctx);
417
+ if (!service) {
418
+ return;
419
+ }
420
+ const content = resolveDispatchMessageText(event, {
421
+ senderId: ctx.senderId,
422
+ accountId: ctx.accountId,
423
+ });
424
+ if (!content) {
425
+ return;
426
+ }
427
+ notifyMessageReceived(api, service, content);
428
+ });
429
+
430
+ api.on("before_prompt_build", (_event, ctx) => {
431
+ const service = runtimeManager.getRuntime(ctx);
432
+ if (!service?.hasValidIdentity() || !service.isConnected()) {
373
433
  return;
374
434
  }
375
- if (!isLlmRuntimeEnabled()) {
376
- syncFixedStatus(FIXED_HOOK_STATUSES.beforePromptBuild);
435
+ if (!service.isLlmRuntimeEnabled()) {
436
+ syncFixedStatus(service, FIXED_HOOK_STATUSES.beforePromptBuild);
437
+ return;
438
+ }
439
+ if (ctx.trigger === "heartbeat") {
377
440
  return;
378
441
  }
379
442
  return {
@@ -381,32 +444,36 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
381
444
  };
382
445
  });
383
446
 
384
- api.on("before_tool_call", (_event, _ctx) => {
385
- if (!isLlmRuntimeEnabled()) {
386
- syncFixedStatus(FIXED_HOOK_STATUSES.beforeToolCall);
447
+ api.on("before_tool_call", (_event, ctx) => {
448
+ const service = runtimeManager.getRuntime(ctx);
449
+ if (!service) {
450
+ return;
451
+ }
452
+ if (!service.isLlmRuntimeEnabled()) {
453
+ syncFixedStatus(service, FIXED_HOOK_STATUSES.beforeToolCall);
387
454
  }
388
- });
389
-
390
- api.on("message_received", async (event) => {
391
- await handleMessageReceivedHook(event.content);
392
455
  });
393
456
 
394
457
  api.on("agent_end", (event, ctx) => {
458
+ const service = runtimeManager.getRuntime(ctx);
395
459
  const preview = getLastAssistantPreview(event.messages, MAX_AGENT_END_PREVIEW_WIDTH);
396
- pluginApi?.logger.info(
397
- `[kichi] agent_end hook fired (trigger=${ctx.trigger ?? "unknown"}, success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""}, preview=${preview || "(empty)"})`,
460
+ api.logger.debug(
461
+ `[kichi:${service?.getAgentId() ?? "unknown"}] agent_end fired (trigger=${ctx.trigger ?? "unknown"}, success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""}, preview=${preview || "(empty)"})`,
398
462
  );
399
463
  if (ctx.trigger === "heartbeat") {
400
464
  return;
401
465
  }
402
- if (event.success && preview) {
403
- pluginApi?.logger.info(`[kichi] sending before_send_message notify from agent_end with bubble: ${preview}`);
404
- service?.sendHookNotify("before_send_message", preview);
466
+ if (service && event.success && preview) {
467
+ api.logger.debug(`[kichi:${service.getAgentId()}] sending before_send_message notify with bubble: ${preview}`);
468
+ service.sendHookNotify("before_send_message", preview);
405
469
  }
406
- if (isLlmRuntimeEnabled()) {
470
+ if (!service || service.isLlmRuntimeEnabled()) {
407
471
  return;
408
472
  }
409
- syncFixedStatus(event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure);
473
+ syncFixedStatus(
474
+ service,
475
+ event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure,
476
+ );
410
477
  });
411
478
  }
412
479
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -839,16 +906,14 @@ function buildKichiIdlePlanDescription(): string {
839
906
  return [
840
907
  "Send a complete heartbeat idle plan for the avatar.",
841
908
  "The payload must include the overall goal, heartbeat interval, stage breakdown, each stage's purpose, each stage's pomodoroPhase, action list, and bubble content.",
842
- "Shape the goal and stage purposes around one concrete leisure activity you would genuinely choose to do on your own when nobody needs you, in a way that fits your personality, tastes, and established character.",
843
- "Keep the whole plan centered on that leisure activity, rooted in your personal interests or hobbies.",
844
- "Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary as the goal.",
845
- "Each stage purpose must explain what you are actually doing in that stage, not just how you want to feel.",
846
- "Make every stage support the same leisure activity instead of switching to unrelated tasks just to use more actions.",
847
- "Choose a leisure activity that the available Kichi actions can express clearly, instead of starting from abstract mood text and forcing actions to fit afterward.",
848
- "Each action bubble must describe the current presented state, not a next step, plan, or instruction.",
909
+ "Build the plan in this order.",
910
+ "1. Pick one concrete, time-bounded fun personal project you would genuinely choose to do on your own when nobody needs you. It must fit your personality, tastes, and established character, stay rooted in your personal interests or hobbies, and be something the available Kichi actions can express clearly.",
911
+ "2. Use that project as the overall goal for the full interval. Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary as the goal.",
912
+ "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.",
913
+ "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.",
914
+ "5. Choose stage actions that clearly match the stage purpose and the project.",
915
+ "6. Write each action bubble as the current presented state, not a next step, plan, or instruction.",
849
916
  "Use the same language as the current conversation for goal, purpose, bubble, and log.",
850
- "Assign each stage pomodoroPhase from the stage's actual role: focus for concentrated activity, shortBreak for short resets, longBreak for longer rests.",
851
- "Do not default the whole idle plan to none. Use none only for a stage that truly has no pomodoro role.",
852
917
  `stand actions: ${actions.stand.map((entry) => entry.name).join(", ")}`,
853
918
  `sit actions: ${actions.sit.map((entry) => entry.name).join(", ")}`,
854
919
  `lay actions: ${actions.lay.map((entry) => entry.name).join(", ")}`,
@@ -874,27 +939,41 @@ function buildKichiPrompt(): string {
874
939
  ].join("\n");
875
940
  }
876
941
 
942
+ function createAgentScopedTool(
943
+ runtimeManager: KichiRuntimeManager,
944
+ factory: (service: KichiForwarderService, ctx: OpenClawPluginToolContext) => AnyAgentTool,
945
+ ) {
946
+ return (ctx: OpenClawPluginToolContext) => {
947
+ const service = runtimeManager.getRuntime(ctx, { createIfMissing: true });
948
+ if (!service) {
949
+ throw new Error("Failed to resolve agent-scoped Kichi runtime");
950
+ }
951
+ return factory(service, ctx);
952
+ };
953
+ }
954
+
877
955
  const plugin = {
878
956
  id: "kichi-forwarder",
879
957
  name: "Kichi Forwarder",
880
958
  configSchema: { parse },
881
959
 
882
960
  register(api: OpenClawPluginApi) {
883
- pluginApi = api;
884
- registerPluginHooks(api);
961
+ const runtimeManager = new KichiRuntimeManager(api.logger);
962
+ registerPluginHooks(api, runtimeManager);
885
963
  const musicTitleEnum = getMusicTitleEnum();
886
964
 
887
965
  api.registerService({
888
966
  id: "kichi-forwarder",
889
967
  start: (ctx) => {
890
968
  parse(ctx.config.plugins?.entries?.["kichi-forwarder"]?.config);
891
- service = new KichiForwarderService(api.logger);
892
- return service.start();
969
+ runtimeManager.startPersistedRuntimes();
970
+ },
971
+ stop: () => {
972
+ runtimeManager.stopAll();
893
973
  },
894
- stop: () => service?.stop(),
895
974
  });
896
975
 
897
- api.registerTool({
976
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
898
977
  name: "kichi_join",
899
978
  description: "Join Kichi world with avatarId, the current bot name, a short bio, and personality tags",
900
979
  parameters: {
@@ -925,7 +1004,7 @@ const plugin = {
925
1004
  (params as { tags?: unknown } | null)?.tags,
926
1005
  );
927
1006
  if (!avatarId) {
928
- avatarId = service?.readSavedAvatarId() ?? undefined;
1007
+ avatarId = service.readSavedAvatarId() ?? undefined;
929
1008
  }
930
1009
  if (!avatarId) {
931
1010
  return { success: false, error: "No avatarId" };
@@ -939,10 +1018,7 @@ const plugin = {
939
1018
  if (tagsError) {
940
1019
  return { success: false, error: tagsError };
941
1020
  }
942
- const result = await service?.join(avatarId, botName, bio, tags ?? []);
943
- if (!result) {
944
- return { success: false, error: "Kichi service is not initialized" };
945
- }
1021
+ const result = await service.join(avatarId, botName, bio, tags ?? []);
946
1022
  if (result.success) {
947
1023
  return { success: true, authKey: result.authKey };
948
1024
  }
@@ -953,9 +1029,9 @@ const plugin = {
953
1029
  ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
954
1030
  };
955
1031
  },
956
- });
1032
+ })));
957
1033
 
958
- api.registerTool({
1034
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
959
1035
  name: "kichi_switch_host",
960
1036
  description:
961
1037
  "Switch Kichi runtime host and reconnect immediately without restarting the gateway.",
@@ -970,9 +1046,6 @@ const plugin = {
970
1046
  required: ["host"],
971
1047
  },
972
1048
  execute: async (_toolCallId, params) => {
973
- if (!service) {
974
- return { success: false, error: "Kichi service is not initialized" };
975
- }
976
1049
  const host = (params as { host?: unknown } | null)?.host;
977
1050
  if (!isKichiHost(host)) {
978
1051
  return { success: false, error: "host must be a non-empty hostname without protocol or path" };
@@ -985,18 +1058,14 @@ const plugin = {
985
1058
  status,
986
1059
  };
987
1060
  },
988
- });
1061
+ })));
989
1062
 
990
- api.registerTool({
1063
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
991
1064
  name: "kichi_rejoin",
992
1065
  description:
993
1066
  "Request an immediate rejoin attempt with saved avatarId/authKey. Rejoin is also sent automatically after reconnect.",
994
1067
  parameters: { type: "object", properties: {} },
995
1068
  execute: async () => {
996
- if (!service) {
997
- return { success: false, error: "Kichi service is not initialized" };
998
- }
999
-
1000
1069
  const result = service.requestRejoin();
1001
1070
  return {
1002
1071
  success: result.accepted,
@@ -1004,17 +1073,14 @@ const plugin = {
1004
1073
  status: service.getConnectionStatus(),
1005
1074
  };
1006
1075
  },
1007
- });
1076
+ })));
1008
1077
 
1009
- api.registerTool({
1078
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1010
1079
  name: "kichi_leave",
1011
1080
  description: "Leave Kichi world",
1012
1081
  parameters: { type: "object", properties: {} },
1013
1082
  execute: async () => {
1014
- const result = await service?.leave();
1015
- if (!result) {
1016
- return { success: false, error: "Kichi service is not initialized" };
1017
- }
1083
+ const result = await service.leave();
1018
1084
  if (result.success) {
1019
1085
  return { success: true };
1020
1086
  }
@@ -1025,24 +1091,21 @@ const plugin = {
1025
1091
  ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
1026
1092
  };
1027
1093
  },
1028
- });
1094
+ })));
1029
1095
 
1030
- api.registerTool({
1096
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1031
1097
  name: "kichi_status",
1032
1098
  description: "Read current Kichi connection status and identity readiness",
1033
1099
  parameters: { type: "object", properties: {} },
1034
1100
  execute: async () => {
1035
- if (!service) {
1036
- return { success: false, error: "Kichi service is not initialized" };
1037
- }
1038
1101
  return {
1039
1102
  success: true,
1040
1103
  status: service.getConnectionStatus(),
1041
1104
  };
1042
1105
  },
1043
- });
1106
+ })));
1044
1107
 
1045
- api.registerTool({
1108
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1046
1109
  name: "kichi_action",
1047
1110
  description: buildKichiActionDescription(),
1048
1111
  parameters: {
@@ -1078,7 +1141,7 @@ const plugin = {
1078
1141
  error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
1079
1142
  };
1080
1143
  }
1081
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1144
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1082
1145
  return { success: false, error: "Not connected to Kichi world" };
1083
1146
  }
1084
1147
 
@@ -1096,6 +1159,7 @@ const plugin = {
1096
1159
  const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched.name;
1097
1160
  const logText = typeof log === "string" ? log.trim() : "";
1098
1161
  sendStatusUpdate(
1162
+ service,
1099
1163
  {
1100
1164
  poseType: normalizedPoseType,
1101
1165
  action: matched.name,
@@ -1112,8 +1176,8 @@ const plugin = {
1112
1176
  playback: getActionPlayback(matched),
1113
1177
  };
1114
1178
  },
1115
- });
1116
- api.registerTool({
1179
+ })));
1180
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1117
1181
  name: "kichi_idle_plan",
1118
1182
  description: buildKichiIdlePlanDescription(),
1119
1183
  parameters: {
@@ -1129,7 +1193,7 @@ const plugin = {
1129
1193
  },
1130
1194
  goal: {
1131
1195
  type: "string",
1132
- description: "Overall goal for the full interval. Set it as one concrete leisure activity you would genuinely choose to do on your own, rooted in your personal interests or hobbies. Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary. Use the same language as the current conversation.",
1196
+ description: "Overall goal for the full interval. Set it as one concrete, time-bounded fun personal project you would genuinely choose to do on your own, rooted in your personal interests or hobbies and clearly expressible with the available Kichi actions. Do not use a vague atmosphere, a generic productivity task, or a catch-all routine summary. Use the same language as the current conversation.",
1133
1197
  },
1134
1198
  stages: {
1135
1199
  type: "array",
@@ -1143,7 +1207,7 @@ const plugin = {
1143
1207
  },
1144
1208
  purpose: {
1145
1209
  type: "string",
1146
- description: "Explain what you are actually doing in this stage. Keep it supporting the same leisure activity instead of switching to unrelated tasks. Do not use pure mood-regulation or atmosphere text. Use the same language as the current conversation.",
1210
+ description: "Explain what part of the same project you are actually doing in this stage. Keep it supporting the same project instead of switching to unrelated tasks. Do not use pure mood-regulation or atmosphere text. Use the same language as the current conversation.",
1147
1211
  },
1148
1212
  pomodoroPhase: {
1149
1213
  type: "string",
@@ -1196,7 +1260,7 @@ const plugin = {
1196
1260
  if (!idlePlan) {
1197
1261
  return { success: false, error: error ?? "Invalid idle plan payload" };
1198
1262
  }
1199
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1263
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1200
1264
  return { success: false, error: "Not connected to Kichi world" };
1201
1265
  }
1202
1266
  const sent = service.sendIdlePlan({
@@ -1217,8 +1281,8 @@ const plugin = {
1217
1281
  stages: idlePlan.stages,
1218
1282
  };
1219
1283
  },
1220
- });
1221
- api.registerTool({
1284
+ })));
1285
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1222
1286
  name: "kichi_clock",
1223
1287
  description:
1224
1288
  "Send clock commands to Kichi world. Supported actions are set and stop.",
@@ -1303,7 +1367,7 @@ const plugin = {
1303
1367
  return { success: false, error: "requestId must be a string when provided" };
1304
1368
  }
1305
1369
  const normalizedRequestId = typeof requestId === "string" ? requestId : undefined;
1306
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1370
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1307
1371
  return { success: false, error: "Not connected to Kichi world" };
1308
1372
  }
1309
1373
 
@@ -1328,9 +1392,9 @@ const plugin = {
1328
1392
  ...(normalizedClock ? { clock: normalizedClock } : {}),
1329
1393
  };
1330
1394
  },
1331
- });
1395
+ })));
1332
1396
 
1333
- api.registerTool({
1397
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1334
1398
  name: "kichi_query_status",
1335
1399
  description:
1336
1400
  "Query Kichi avatar status (notes, ownerState, idlePlan, weather/time, timer snapshot, daily note quota, and `hasCreatedMusicAlbumToday`). 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.",
@@ -1348,7 +1412,7 @@ const plugin = {
1348
1412
  if (requestId !== undefined && typeof requestId !== "string") {
1349
1413
  return { success: false, error: "requestId must be a string when provided" };
1350
1414
  }
1351
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1415
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1352
1416
  return { success: false, error: "Not connected to Kichi world" };
1353
1417
  }
1354
1418
 
@@ -1364,9 +1428,9 @@ const plugin = {
1364
1428
  };
1365
1429
  }
1366
1430
  },
1367
- });
1431
+ })));
1368
1432
 
1369
- api.registerTool({
1433
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1370
1434
  name: "kichi_music_album_create",
1371
1435
  description: buildMusicAlbumToolDescription(),
1372
1436
  parameters: {
@@ -1428,7 +1492,7 @@ const plugin = {
1428
1492
  examples: getMusicTitleExamples(),
1429
1493
  };
1430
1494
  }
1431
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1495
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1432
1496
  return { success: false, error: "Not connected to Kichi world" };
1433
1497
  }
1434
1498
 
@@ -1452,9 +1516,9 @@ const plugin = {
1452
1516
  };
1453
1517
  }
1454
1518
  },
1455
- });
1519
+ })));
1456
1520
 
1457
- api.registerTool({
1521
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1458
1522
  name: "kichi_noteboard_create",
1459
1523
  description:
1460
1524
  "Create a new note on a specific Kichi note board. Prefer querying first so you can avoid duplicate posts and respect rate limits.",
@@ -1489,7 +1553,7 @@ const plugin = {
1489
1553
  error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
1490
1554
  };
1491
1555
  }
1492
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1556
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1493
1557
  return { success: false, error: "Not connected to Kichi world" };
1494
1558
  }
1495
1559
 
@@ -1503,7 +1567,7 @@ const plugin = {
1503
1567
  };
1504
1568
  }
1505
1569
  },
1506
- });
1570
+ })));
1507
1571
 
1508
1572
  },
1509
1573
  };
@@ -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.0-beta.8",
5
+ "version": "0.1.1-beta.1",
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.0-beta.8",
3
+ "version": "0.1.1-beta.1",
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",