@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 +6 -6
- package/config/kichi-config.json +0 -4
- package/index.ts +190 -126
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +15 -15
- package/skills/kichi-forwarder/references/heartbeat.md +32 -52
- package/skills/kichi-forwarder/references/install.md +22 -17
- package/src/runtime-manager.ts +223 -0
- package/src/service.ts +77 -30
- 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/config/kichi-config.json
CHANGED
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,65 @@ 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 registerPluginHooks(api: OpenClawPluginApi): void {
|
|
371
|
-
api.on("
|
|
372
|
-
|
|
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,
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
397
|
-
`[kichi] agent_end
|
|
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
|
-
|
|
404
|
-
service
|
|
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(
|
|
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
|
-
"
|
|
843
|
-
"
|
|
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
|
|
847
|
-
"Choose
|
|
848
|
-
"
|
|
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
|
-
|
|
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
|
-
|
|
892
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
};
|
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.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.
|
|
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",
|