@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 +6 -6
- package/index.ts +265 -113
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +6 -6
- package/skills/kichi-forwarder/references/install.md +22 -18
- package/src/runtime-manager.ts +240 -0
- package/src/service.ts +134 -44
- package/src/types.ts +3 -0
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
|
|
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 {
|
|
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
|
|
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
|
|
237
|
-
|
|
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
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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,
|
|
388
|
-
|
|
389
|
-
|
|
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
|
-
|
|
400
|
-
`[kichi] agent_end
|
|
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
|
-
|
|
407
|
-
service
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
893
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|
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.
|
|
5
|
+
"version": "0.1.1-beta.2",
|
|
6
6
|
"author": "OpenClaw",
|
|
7
7
|
"skills": ["./skills/kichi-forwarder"],
|
|
8
8
|
"configSchema": {
|