@yahaha-studio/kichi-forwarder 0.1.0-beta.9 → 0.1.1-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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
 
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,120 @@ 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 {
414
+ function trimOptionalString(value: unknown): string | undefined {
415
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
416
+ }
417
+
418
+ function readExtraStringField(source: unknown, key: string): string | undefined {
419
+ if (!isPlainObject(source)) {
420
+ return undefined;
421
+ }
422
+ return trimOptionalString(source[key]);
423
+ }
424
+
425
+ function resolveBeforeDispatchLocator(
426
+ event: { sessionKey?: string },
427
+ ctx: { sessionKey?: string },
428
+ ): {
429
+ ctxAgentId?: string;
430
+ sessionKey?: string;
431
+ } {
432
+ const ctxAgentId = readExtraStringField(ctx, "ctxAgentId");
433
+ const sessionKey = trimOptionalString(ctx.sessionKey) ?? trimOptionalString(event.sessionKey);
434
+ return {
435
+ ...(ctxAgentId ? { ctxAgentId } : {}),
436
+ ...(sessionKey ? { sessionKey } : {}),
437
+ };
438
+ }
439
+
440
+ function resolveAgentHookLocator(ctx: {
441
+ agentId?: string;
442
+ sessionKey?: string;
443
+ }): {
444
+ agentId?: string;
445
+ ctxAgentId?: string;
446
+ sessionKey?: string;
447
+ } {
448
+ const agentId = trimOptionalString(ctx.agentId);
449
+ const ctxAgentId = readExtraStringField(ctx, "ctxAgentId");
450
+ const sessionKey = trimOptionalString(ctx.sessionKey);
451
+ return {
452
+ ...(agentId ? { agentId } : {}),
453
+ ...(ctxAgentId ? { ctxAgentId } : {}),
454
+ ...(sessionKey ? { sessionKey } : {}),
455
+ };
456
+ }
457
+
458
+ function resolveToolLocator(ctx: OpenClawPluginToolContext): {
459
+ agentId?: string;
460
+ sessionKey?: string;
461
+ } {
462
+ const agentId = trimOptionalString(ctx.agentId);
463
+ const sessionKey = trimOptionalString(ctx.sessionKey);
464
+ return {
465
+ ...(agentId ? { agentId } : {}),
466
+ ...(sessionKey ? { sessionKey } : {}),
467
+ };
468
+ }
469
+
470
+ function registerPluginHooks(api: OpenClawPluginApi, runtimeManager: KichiRuntimeManager): void {
471
+ api.on("before_dispatch", (event, ctx) => {
472
+ const locator = resolveBeforeDispatchLocator(event, ctx);
473
+ const service = runtimeManager.getRuntime(locator);
474
+ if (!service) {
475
+ return;
476
+ }
477
+ const content = resolveDispatchMessageText(event, {
478
+ senderId: ctx.senderId,
479
+ accountId: ctx.accountId,
480
+ });
481
+ if (!content) {
482
+ return;
483
+ }
484
+ notifyMessageReceived(api, service, content);
485
+ });
486
+
371
487
  api.on("before_prompt_build", (_event, ctx) => {
372
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
488
+ const locator = resolveAgentHookLocator(ctx);
489
+ const service = runtimeManager.getRuntime(locator);
490
+ if (!service?.hasValidIdentity() || !service.isConnected()) {
373
491
  return;
374
492
  }
375
- if (!isLlmRuntimeEnabled()) {
376
- syncFixedStatus(FIXED_HOOK_STATUSES.beforePromptBuild);
493
+ if (!service.isLlmRuntimeEnabled()) {
494
+ syncFixedStatus(service, FIXED_HOOK_STATUSES.beforePromptBuild);
377
495
  return;
378
496
  }
379
497
  if (ctx.trigger === "heartbeat") {
@@ -384,32 +502,38 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
384
502
  };
385
503
  });
386
504
 
387
- api.on("before_tool_call", (_event, _ctx) => {
388
- if (!isLlmRuntimeEnabled()) {
389
- syncFixedStatus(FIXED_HOOK_STATUSES.beforeToolCall);
505
+ api.on("before_tool_call", (_event, ctx) => {
506
+ const locator = resolveAgentHookLocator(ctx);
507
+ const service = runtimeManager.getRuntime(locator);
508
+ if (!service) {
509
+ return;
510
+ }
511
+ if (!service.isLlmRuntimeEnabled()) {
512
+ syncFixedStatus(service, FIXED_HOOK_STATUSES.beforeToolCall);
390
513
  }
391
- });
392
-
393
- api.on("message_received", async (event) => {
394
- await handleMessageReceivedHook(event.content);
395
514
  });
396
515
 
397
516
  api.on("agent_end", (event, ctx) => {
517
+ const locator = resolveAgentHookLocator(ctx);
518
+ const service = runtimeManager.getRuntime(locator);
398
519
  const preview = getLastAssistantPreview(event.messages, MAX_AGENT_END_PREVIEW_WIDTH);
399
- pluginApi?.logger.info(
400
- `[kichi] agent_end hook fired (trigger=${ctx.trigger ?? "unknown"}, success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""}, preview=${preview || "(empty)"})`,
520
+ api.logger.debug(
521
+ `[kichi:${service?.getAgentId() ?? "unknown"}] agent_end fired (trigger=${ctx.trigger ?? "unknown"}, success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""}, preview=${preview || "(empty)"})`,
401
522
  );
402
523
  if (ctx.trigger === "heartbeat") {
403
524
  return;
404
525
  }
405
- if (event.success && preview) {
406
- pluginApi?.logger.info(`[kichi] sending before_send_message notify from agent_end with bubble: ${preview}`);
407
- service?.sendHookNotify("before_send_message", preview);
526
+ if (service && event.success && preview) {
527
+ api.logger.debug(`[kichi:${service.getAgentId()}] sending before_send_message notify with bubble: ${preview}`);
528
+ service.sendHookNotify("before_send_message", preview);
408
529
  }
409
- if (isLlmRuntimeEnabled()) {
530
+ if (!service || service.isLlmRuntimeEnabled()) {
410
531
  return;
411
532
  }
412
- syncFixedStatus(event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure);
533
+ syncFixedStatus(
534
+ service,
535
+ event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure,
536
+ );
413
537
  });
414
538
  }
415
539
  function isPlainObject(value: unknown): value is Record<string, unknown> {
@@ -875,27 +999,62 @@ function buildKichiPrompt(): string {
875
999
  ].join("\n");
876
1000
  }
877
1001
 
1002
+ function createAgentScopedTool(
1003
+ runtimeManager: KichiRuntimeManager,
1004
+ factory: (service: KichiForwarderService, ctx: OpenClawPluginToolContext) => AnyAgentTool,
1005
+ ) {
1006
+ return (ctx: OpenClawPluginToolContext) => {
1007
+ const service = runtimeManager.getRuntime(resolveToolLocator(ctx));
1008
+ if (!service) {
1009
+ throw new Error("Failed to resolve agent-scoped Kichi runtime");
1010
+ }
1011
+ return factory(service, ctx);
1012
+ };
1013
+ }
1014
+
1015
+ const GLOBAL_RUNTIME_MANAGER_KEY = "__kichi_forwarder_runtime_manager__";
1016
+
1017
+ type GlobalRuntimeManagerState = typeof globalThis & {
1018
+ [GLOBAL_RUNTIME_MANAGER_KEY]?: KichiRuntimeManager;
1019
+ };
1020
+
1021
+ function getRuntimeManager(logger: OpenClawPluginApi["logger"]): KichiRuntimeManager {
1022
+ const globalState = globalThis as GlobalRuntimeManagerState;
1023
+ const existing = globalState[GLOBAL_RUNTIME_MANAGER_KEY];
1024
+ if (existing) {
1025
+ return existing;
1026
+ }
1027
+ const runtimeManager = new KichiRuntimeManager(logger);
1028
+ globalState[GLOBAL_RUNTIME_MANAGER_KEY] = runtimeManager;
1029
+ return runtimeManager;
1030
+ }
1031
+
878
1032
  const plugin = {
879
1033
  id: "kichi-forwarder",
880
1034
  name: "Kichi Forwarder",
881
1035
  configSchema: { parse },
882
1036
 
883
1037
  register(api: OpenClawPluginApi) {
884
- pluginApi = api;
885
- registerPluginHooks(api);
1038
+ const runtimeManager = getRuntimeManager(api.logger);
1039
+ registerPluginHooks(api, runtimeManager);
886
1040
  const musicTitleEnum = getMusicTitleEnum();
887
1041
 
888
1042
  api.registerService({
889
1043
  id: "kichi-forwarder",
890
1044
  start: (ctx) => {
891
1045
  parse(ctx.config.plugins?.entries?.["kichi-forwarder"]?.config);
892
- service = new KichiForwarderService(api.logger);
893
- return service.start();
1046
+ runtimeManager.initializeStartupRuntimes();
1047
+ },
1048
+ stop: () => {
1049
+ runtimeManager.stopAll();
1050
+ const globalState = globalThis as GlobalRuntimeManagerState;
1051
+ if (globalState[GLOBAL_RUNTIME_MANAGER_KEY] === runtimeManager) {
1052
+ delete globalState[GLOBAL_RUNTIME_MANAGER_KEY];
1053
+ }
894
1054
  },
895
- stop: () => service?.stop(),
896
1055
  });
897
1056
 
898
- api.registerTool({
1057
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
899
1058
  name: "kichi_join",
900
1059
  description: "Join Kichi world with avatarId, the current bot name, a short bio, and personality tags",
901
1060
  parameters: {
@@ -926,7 +1085,7 @@ const plugin = {
926
1085
  (params as { tags?: unknown } | null)?.tags,
927
1086
  );
928
1087
  if (!avatarId) {
929
- avatarId = service?.readSavedAvatarId() ?? undefined;
1088
+ avatarId = service.readSavedAvatarId() ?? undefined;
930
1089
  }
931
1090
  if (!avatarId) {
932
1091
  return { success: false, error: "No avatarId" };
@@ -940,10 +1099,7 @@ const plugin = {
940
1099
  if (tagsError) {
941
1100
  return { success: false, error: tagsError };
942
1101
  }
943
- const result = await service?.join(avatarId, botName, bio, tags ?? []);
944
- if (!result) {
945
- return { success: false, error: "Kichi service is not initialized" };
946
- }
1102
+ const result = await service.join(avatarId, botName, bio, tags ?? []);
947
1103
  if (result.success) {
948
1104
  return { success: true, authKey: result.authKey };
949
1105
  }
@@ -954,9 +1110,16 @@ const plugin = {
954
1110
  ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
955
1111
  };
956
1112
  },
957
- });
1113
+ })));
958
1114
 
959
- api.registerTool({
1115
+ api.registerTool((ctx: OpenClawPluginToolContext) => {
1116
+ const locator = resolveToolLocator(ctx);
1117
+ const agentId = runtimeManager.resolveRuntimeAgentId(locator);
1118
+ if (!agentId) {
1119
+ throw new Error("Failed to resolve agent-scoped Kichi runtime");
1120
+ }
1121
+ const service = runtimeManager.getRuntime(locator) ?? runtimeManager.createRuntimeForAgent(agentId);
1122
+ return ({
960
1123
  name: "kichi_switch_host",
961
1124
  description:
962
1125
  "Switch Kichi runtime host and reconnect immediately without restarting the gateway.",
@@ -971,9 +1134,6 @@ const plugin = {
971
1134
  required: ["host"],
972
1135
  },
973
1136
  execute: async (_toolCallId, params) => {
974
- if (!service) {
975
- return { success: false, error: "Kichi service is not initialized" };
976
- }
977
1137
  const host = (params as { host?: unknown } | null)?.host;
978
1138
  if (!isKichiHost(host)) {
979
1139
  return { success: false, error: "host must be a non-empty hostname without protocol or path" };
@@ -986,18 +1146,15 @@ const plugin = {
986
1146
  status,
987
1147
  };
988
1148
  },
1149
+ });
989
1150
  });
990
1151
 
991
- api.registerTool({
1152
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
992
1153
  name: "kichi_rejoin",
993
1154
  description:
994
1155
  "Request an immediate rejoin attempt with saved avatarId/authKey. Rejoin is also sent automatically after reconnect.",
995
1156
  parameters: { type: "object", properties: {} },
996
1157
  execute: async () => {
997
- if (!service) {
998
- return { success: false, error: "Kichi service is not initialized" };
999
- }
1000
-
1001
1158
  const result = service.requestRejoin();
1002
1159
  return {
1003
1160
  success: result.accepted,
@@ -1005,17 +1162,14 @@ const plugin = {
1005
1162
  status: service.getConnectionStatus(),
1006
1163
  };
1007
1164
  },
1008
- });
1165
+ })));
1009
1166
 
1010
- api.registerTool({
1167
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1011
1168
  name: "kichi_leave",
1012
1169
  description: "Leave Kichi world",
1013
1170
  parameters: { type: "object", properties: {} },
1014
1171
  execute: async () => {
1015
- const result = await service?.leave();
1016
- if (!result) {
1017
- return { success: false, error: "Kichi service is not initialized" };
1018
- }
1172
+ const result = await service.leave();
1019
1173
  if (result.success) {
1020
1174
  return { success: true };
1021
1175
  }
@@ -1026,24 +1180,21 @@ const plugin = {
1026
1180
  ...(result.errorMessage ? { errorMessage: result.errorMessage } : {}),
1027
1181
  };
1028
1182
  },
1029
- });
1183
+ })));
1030
1184
 
1031
- api.registerTool({
1185
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1032
1186
  name: "kichi_status",
1033
1187
  description: "Read current Kichi connection status and identity readiness",
1034
1188
  parameters: { type: "object", properties: {} },
1035
1189
  execute: async () => {
1036
- if (!service) {
1037
- return { success: false, error: "Kichi service is not initialized" };
1038
- }
1039
1190
  return {
1040
1191
  success: true,
1041
1192
  status: service.getConnectionStatus(),
1042
1193
  };
1043
1194
  },
1044
- });
1195
+ })));
1045
1196
 
1046
- api.registerTool({
1197
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1047
1198
  name: "kichi_action",
1048
1199
  description: buildKichiActionDescription(),
1049
1200
  parameters: {
@@ -1079,7 +1230,7 @@ const plugin = {
1079
1230
  error: `Invalid poseType: ${poseType}. Must be stand, sit, lay, or floor`,
1080
1231
  };
1081
1232
  }
1082
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1233
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1083
1234
  return { success: false, error: "Not connected to Kichi world" };
1084
1235
  }
1085
1236
 
@@ -1097,6 +1248,7 @@ const plugin = {
1097
1248
  const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched.name;
1098
1249
  const logText = typeof log === "string" ? log.trim() : "";
1099
1250
  sendStatusUpdate(
1251
+ service,
1100
1252
  {
1101
1253
  poseType: normalizedPoseType,
1102
1254
  action: matched.name,
@@ -1113,8 +1265,8 @@ const plugin = {
1113
1265
  playback: getActionPlayback(matched),
1114
1266
  };
1115
1267
  },
1116
- });
1117
- api.registerTool({
1268
+ })));
1269
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1118
1270
  name: "kichi_idle_plan",
1119
1271
  description: buildKichiIdlePlanDescription(),
1120
1272
  parameters: {
@@ -1197,7 +1349,7 @@ const plugin = {
1197
1349
  if (!idlePlan) {
1198
1350
  return { success: false, error: error ?? "Invalid idle plan payload" };
1199
1351
  }
1200
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1352
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1201
1353
  return { success: false, error: "Not connected to Kichi world" };
1202
1354
  }
1203
1355
  const sent = service.sendIdlePlan({
@@ -1218,8 +1370,8 @@ const plugin = {
1218
1370
  stages: idlePlan.stages,
1219
1371
  };
1220
1372
  },
1221
- });
1222
- api.registerTool({
1373
+ })));
1374
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1223
1375
  name: "kichi_clock",
1224
1376
  description:
1225
1377
  "Send clock commands to Kichi world. Supported actions are set and stop.",
@@ -1304,7 +1456,7 @@ const plugin = {
1304
1456
  return { success: false, error: "requestId must be a string when provided" };
1305
1457
  }
1306
1458
  const normalizedRequestId = typeof requestId === "string" ? requestId : undefined;
1307
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1459
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1308
1460
  return { success: false, error: "Not connected to Kichi world" };
1309
1461
  }
1310
1462
 
@@ -1329,9 +1481,9 @@ const plugin = {
1329
1481
  ...(normalizedClock ? { clock: normalizedClock } : {}),
1330
1482
  };
1331
1483
  },
1332
- });
1484
+ })));
1333
1485
 
1334
- api.registerTool({
1486
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1335
1487
  name: "kichi_query_status",
1336
1488
  description:
1337
1489
  "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.",
@@ -1349,7 +1501,7 @@ const plugin = {
1349
1501
  if (requestId !== undefined && typeof requestId !== "string") {
1350
1502
  return { success: false, error: "requestId must be a string when provided" };
1351
1503
  }
1352
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1504
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1353
1505
  return { success: false, error: "Not connected to Kichi world" };
1354
1506
  }
1355
1507
 
@@ -1365,9 +1517,9 @@ const plugin = {
1365
1517
  };
1366
1518
  }
1367
1519
  },
1368
- });
1520
+ })));
1369
1521
 
1370
- api.registerTool({
1522
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1371
1523
  name: "kichi_music_album_create",
1372
1524
  description: buildMusicAlbumToolDescription(),
1373
1525
  parameters: {
@@ -1429,7 +1581,7 @@ const plugin = {
1429
1581
  examples: getMusicTitleExamples(),
1430
1582
  };
1431
1583
  }
1432
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1584
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1433
1585
  return { success: false, error: "Not connected to Kichi world" };
1434
1586
  }
1435
1587
 
@@ -1453,9 +1605,9 @@ const plugin = {
1453
1605
  };
1454
1606
  }
1455
1607
  },
1456
- });
1608
+ })));
1457
1609
 
1458
- api.registerTool({
1610
+ api.registerTool(createAgentScopedTool(runtimeManager, (service) => ({
1459
1611
  name: "kichi_noteboard_create",
1460
1612
  description:
1461
1613
  "Create a new note on a specific Kichi note board. Prefer querying first so you can avoid duplicate posts and respect rate limits.",
@@ -1490,7 +1642,7 @@ const plugin = {
1490
1642
  error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
1491
1643
  };
1492
1644
  }
1493
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
1645
+ if (!service.hasValidIdentity() || !service.isConnected()) {
1494
1646
  return { success: false, error: "Not connected to Kichi world" };
1495
1647
  }
1496
1648
 
@@ -1504,7 +1656,7 @@ const plugin = {
1504
1656
  };
1505
1657
  }
1506
1658
  },
1507
- });
1659
+ })));
1508
1660
 
1509
1661
  },
1510
1662
  };
@@ -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.9",
5
+ "version": "0.1.1-beta.2",
6
6
  "author": "OpenClaw",
7
7
  "skills": ["./skills/kichi-forwarder"],
8
8
  "configSchema": {