@yahaha-studio/kichi-forwarder 0.0.1-alpha.50 → 0.0.1-alpha.51
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/index.ts +37 -420
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +40 -51
- package/skills/kichi-forwarder/references/error.md +2 -2
- package/skills/kichi-forwarder/references/heartbeat.md +1 -1
- package/skills/kichi-forwarder/references/install.md +29 -40
- package/src/config.ts +1 -12
- package/src/service.ts +32 -59
- package/src/types.ts +3 -25
package/index.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import fs from "node:fs";
|
|
1
|
+
import fs from "node:fs";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
@@ -10,7 +10,6 @@ import type {
|
|
|
10
10
|
Album,
|
|
11
11
|
ClockAction,
|
|
12
12
|
ClockConfig,
|
|
13
|
-
KichiEnv,
|
|
14
13
|
KichiForwarderConfig,
|
|
15
14
|
KichiState,
|
|
16
15
|
KichiStaticConfig,
|
|
@@ -51,194 +50,8 @@ 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;
|
|
54
|
-
const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
55
|
-
|
|
56
|
-
reset: "\u001b[0m",
|
|
57
|
-
bold: "\u001b[1m",
|
|
58
|
-
dim: "\u001b[2m",
|
|
59
|
-
cyan: "\u001b[36m",
|
|
60
|
-
green: "\u001b[32m",
|
|
61
|
-
yellow: "\u001b[33m",
|
|
62
|
-
magenta: "\u001b[35m",
|
|
63
|
-
blue: "\u001b[34m",
|
|
64
|
-
gray: "\u001b[90m",
|
|
65
|
-
white: "\u001b[37m",
|
|
66
|
-
};
|
|
67
|
-
const WORKSPACE_SCREEN_WIDTH = 109;
|
|
68
|
-
const WORKSPACE_ACTIVITY_LIMIT = 8;
|
|
69
|
-
const WORKSPACE_SCREEN_PUSH_DEBOUNCE_MS = 150;
|
|
70
|
-
let cachedStaticConfig: KichiStaticConfig | null = null;
|
|
71
|
-
let cachedStaticConfigMtime = 0;
|
|
72
|
-
let service: KichiForwarderService | null = null;
|
|
73
|
-
let pluginApi: OpenClawPluginApi | null = null;
|
|
74
|
-
let workspaceState: WorkspaceScreenState = createWorkspaceScreenState();
|
|
75
|
-
let workspacePushTimer: NodeJS.Timeout | null = null;
|
|
76
|
-
|
|
77
|
-
type WorkspaceScreenState = {
|
|
78
|
-
sessionLabel: string;
|
|
79
|
-
mode: string;
|
|
80
|
-
phase: string;
|
|
81
|
-
channel: string;
|
|
82
|
-
updatedAtLabel: string;
|
|
83
|
-
recentActivity: string[];
|
|
84
|
-
currentFocus: string;
|
|
85
|
-
hint: string;
|
|
86
|
-
prompt: string;
|
|
87
|
-
title: string;
|
|
88
|
-
shellName: string;
|
|
89
|
-
cwdLabel: string;
|
|
90
|
-
modelLabel: string;
|
|
91
|
-
};
|
|
92
|
-
|
|
93
|
-
function createWorkspaceScreenState(): WorkspaceScreenState {
|
|
94
|
-
return {
|
|
95
|
-
sessionLabel: "main",
|
|
96
|
-
mode: "idle",
|
|
97
|
-
phase: "waiting",
|
|
98
|
-
channel: "unknown",
|
|
99
|
-
updatedAtLabel: "just now",
|
|
100
|
-
recentActivity: [],
|
|
101
|
-
currentFocus: "Waiting for the next thread to pick up.",
|
|
102
|
-
hint: "Low-noise live workspace view.",
|
|
103
|
-
prompt: "$ _",
|
|
104
|
-
title: "Workspace",
|
|
105
|
-
shellName: "agent",
|
|
106
|
-
cwdLabel: process.cwd(),
|
|
107
|
-
modelLabel: "model: unknown",
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
function normalizeShellToken(value: string): string {
|
|
112
|
-
const normalized = value
|
|
113
|
-
.trim()
|
|
114
|
-
.toLowerCase()
|
|
115
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
116
|
-
.replace(/^-+|-+$/g, "");
|
|
117
|
-
return normalized || "agent";
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function parseIdentityNameFromWorkspace(workspaceRoot: string): string | null {
|
|
121
|
-
try {
|
|
122
|
-
const identityPath = path.join(workspaceRoot, "IDENTITY.md");
|
|
123
|
-
if (!fs.existsSync(identityPath)) return null;
|
|
124
|
-
const raw = fs.readFileSync(identityPath, "utf-8");
|
|
125
|
-
const match = raw.match(/^-\s*\*\*Name:\*\*\s*(.+)$/m);
|
|
126
|
-
const value = match?.[1]?.trim();
|
|
127
|
-
return value || null;
|
|
128
|
-
} catch {
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
function deriveWorkspaceIdentity(workspaceRoot: string): Pick<WorkspaceScreenState, "title" | "shellName" | "cwdLabel"> {
|
|
134
|
-
const identityName = parseIdentityNameFromWorkspace(workspaceRoot);
|
|
135
|
-
const titleBase = identityName || path.basename(workspaceRoot) || "workspace";
|
|
136
|
-
return {
|
|
137
|
-
title: `${titleBase} Workspace`,
|
|
138
|
-
shellName: normalizeShellToken(identityName || titleBase),
|
|
139
|
-
cwdLabel: workspaceRoot,
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function nowLabel(): string {
|
|
144
|
-
const date = new Date();
|
|
145
|
-
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
function stripAnsi(text: string): string {
|
|
149
|
-
return text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
function visibleLength(text: string): number {
|
|
153
|
-
return Array.from(stripAnsi(text)).length;
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function padVisible(text: string, width: number): string {
|
|
157
|
-
const pad = Math.max(0, width - visibleLength(text));
|
|
158
|
-
return text + " ".repeat(pad);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
function truncatePlain(text: string, width: number): string {
|
|
162
|
-
const chars = Array.from(text);
|
|
163
|
-
if (chars.length <= width) return text;
|
|
164
|
-
if (width <= 1) return chars.slice(0, width).join("");
|
|
165
|
-
return chars.slice(0, width - 1).join("") + "…";
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function color(text: string, tone: keyof typeof ANSI): string {
|
|
169
|
-
return `${ANSI[tone]}${text}${ANSI.reset}`;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
function pushActivity(line: string): void {
|
|
173
|
-
if (!line.trim()) return;
|
|
174
|
-
const stamped = `[${nowLabel()}] ${line.trim()}`;
|
|
175
|
-
workspaceState.recentActivity.unshift(stamped);
|
|
176
|
-
workspaceState.recentActivity = workspaceState.recentActivity.slice(0, WORKSPACE_ACTIVITY_LIMIT);
|
|
177
|
-
workspaceState.updatedAtLabel = "just now";
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
function renderWorkspaceScreen(): string {
|
|
181
|
-
const innerWidth = WORKSPACE_SCREEN_WIDTH - 2;
|
|
182
|
-
const topTitle = `${color(` ${workspaceState.title} `, "bold")}${color("live session", "gray")}`;
|
|
183
|
-
const topLine = `╭─── ${topTitle}${"─".repeat(Math.max(0, innerWidth - 4 - visibleLength(topTitle)))}╮`;
|
|
184
|
-
const bottomLine = `╰${"─".repeat(innerWidth)}╯`;
|
|
185
|
-
|
|
186
|
-
const body: string[] = [];
|
|
187
|
-
const pushRow = (text = "") => {
|
|
188
|
-
body.push(`│${padVisible(text, innerWidth)}│`);
|
|
189
|
-
};
|
|
190
|
-
|
|
191
|
-
pushRow("");
|
|
192
|
-
pushRow(`${color(" Welcome back.", "white")}`);
|
|
193
|
-
pushRow(`${color(" Current session is active and mirrored as a terminal-style workspace.", "gray")}`);
|
|
194
|
-
pushRow("");
|
|
195
|
-
pushRow(`${color(" Recent activity", "cyan")}`);
|
|
196
|
-
for (const item of workspaceState.recentActivity.length ? workspaceState.recentActivity : ["[--:--:--] idle"] ) {
|
|
197
|
-
pushRow(` ${color("•", "green")} ${truncatePlain(item, innerWidth - 4)}`);
|
|
198
|
-
}
|
|
199
|
-
pushRow("");
|
|
200
|
-
pushRow(`${color(" Current focus", "cyan")}`);
|
|
201
|
-
pushRow(` ${truncatePlain(workspaceState.currentFocus, innerWidth - 2)}`);
|
|
202
|
-
pushRow("");
|
|
203
|
-
pushRow(`${color(" Status", "cyan")}`);
|
|
204
|
-
pushRow(` ${color("mode", "gray")} ${workspaceState.mode}`);
|
|
205
|
-
pushRow(` ${color("phase", "gray")} ${workspaceState.phase}`);
|
|
206
|
-
pushRow(` ${color("channel", "gray")} ${workspaceState.channel}`);
|
|
207
|
-
pushRow(` ${color("session", "gray")} ${workspaceState.sessionLabel}`);
|
|
208
|
-
pushRow("");
|
|
209
|
-
pushRow(`${color(" Hint", "cyan")}`);
|
|
210
|
-
pushRow(` ${truncatePlain(workspaceState.hint, innerWidth - 2)}`);
|
|
211
|
-
pushRow("");
|
|
212
|
-
pushRow(` ${color(workspaceState.modelLabel, "magenta")}`);
|
|
213
|
-
pushRow(` ${color(workspaceState.cwdLabel, "blue")}`);
|
|
214
|
-
pushRow(` ${color(`updated: ${workspaceState.updatedAtLabel}`, "gray")}`);
|
|
215
|
-
pushRow("");
|
|
216
|
-
|
|
217
|
-
return [
|
|
218
|
-
topLine,
|
|
219
|
-
...body,
|
|
220
|
-
bottomLine,
|
|
221
|
-
"",
|
|
222
|
-
color("─".repeat(WORKSPACE_SCREEN_WIDTH), "gray"),
|
|
223
|
-
`${color(workspaceState.shellName, "green")}:${color("~", "blue")} ${workspaceState.prompt}`,
|
|
224
|
-
].join("\n");
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
function scheduleWorkspacePush(): void {
|
|
228
|
-
if (!service?.hasValidIdentity() || !service?.isConnected()) return;
|
|
229
|
-
if (workspacePushTimer) clearTimeout(workspacePushTimer);
|
|
230
|
-
workspacePushTimer = setTimeout(() => {
|
|
231
|
-
workspacePushTimer = null;
|
|
232
|
-
service?.sendWorkspaceScreen(renderWorkspaceScreen(), true);
|
|
233
|
-
}, WORKSPACE_SCREEN_PUSH_DEBOUNCE_MS);
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
function updateWorkspace(partial: Partial<WorkspaceScreenState>, activity?: string): void {
|
|
237
|
-
workspaceState = { ...workspaceState, ...partial, updatedAtLabel: "just now" };
|
|
238
|
-
if (activity) pushActivity(activity);
|
|
239
|
-
scheduleWorkspacePush();
|
|
240
|
-
}
|
|
241
|
-
|
|
53
|
+
const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
54
|
+
|
|
242
55
|
function isAlbumConfig(value: unknown): value is Album {
|
|
243
56
|
if (!value || typeof value !== "object") {
|
|
244
57
|
return false;
|
|
@@ -307,19 +120,19 @@ function normalizeStaticConfig(value: unknown): KichiStaticConfig {
|
|
|
307
120
|
function readState(): KichiState {
|
|
308
121
|
if (!fs.existsSync(STATE_PATH)) {
|
|
309
122
|
return {
|
|
310
|
-
|
|
123
|
+
currentHost: "focus.yahaha.com",
|
|
311
124
|
llmRuntimeEnabled: DEFAULT_LLM_RUNTIME_ENABLED,
|
|
312
125
|
};
|
|
313
126
|
}
|
|
314
127
|
const data = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8")) as Partial<KichiState>;
|
|
315
|
-
if (
|
|
316
|
-
throw new Error(`Invalid
|
|
128
|
+
if (typeof data.currentHost !== "string" || !data.currentHost.trim()) {
|
|
129
|
+
throw new Error(`Invalid currentHost in ${STATE_PATH}`);
|
|
317
130
|
}
|
|
318
131
|
if (typeof data.llmRuntimeEnabled !== "boolean") {
|
|
319
132
|
throw new Error(`Invalid llmRuntimeEnabled in ${STATE_PATH}`);
|
|
320
133
|
}
|
|
321
134
|
return {
|
|
322
|
-
|
|
135
|
+
currentHost: data.currentHost,
|
|
323
136
|
llmRuntimeEnabled: data.llmRuntimeEnabled,
|
|
324
137
|
};
|
|
325
138
|
}
|
|
@@ -476,50 +289,10 @@ async function handleMessageReceivedHook(content: string): Promise<void> {
|
|
|
476
289
|
const trimmed = truncateByDisplayWidth(content, MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH);
|
|
477
290
|
pluginApi?.logger.info(`[kichi] sending message_received notify with preview: ${trimmed || "(empty)"}`);
|
|
478
291
|
service.sendHookNotify("message_received", `"${trimmed}"`);
|
|
479
|
-
updateWorkspace(
|
|
480
|
-
{
|
|
481
|
-
mode: "reading",
|
|
482
|
-
phase: "processing inbound message",
|
|
483
|
-
currentFocus: trimmed ? `User message: ${trimmed}` : "Reading the latest message.",
|
|
484
|
-
hint: "Inbound message updated the live workspace.",
|
|
485
|
-
prompt: "$ reading-message",
|
|
486
|
-
},
|
|
487
|
-
`received message: ${trimmed || "(empty)"}`,
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
|
-
function handleMessageSentHook(): void {
|
|
492
|
-
const connected = service?.isConnected() ?? false;
|
|
493
|
-
const hasIdentity = service?.hasValidIdentity() ?? false;
|
|
494
|
-
pluginApi?.logger.info(`[kichi] message_sent hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
|
|
495
|
-
if (!hasIdentity || !connected) {
|
|
496
|
-
pluginApi?.logger.warn("[kichi] skipped message_sent notify because service is not ready");
|
|
497
|
-
return;
|
|
498
|
-
}
|
|
499
|
-
updateWorkspace(
|
|
500
|
-
{
|
|
501
|
-
mode: "sent",
|
|
502
|
-
phase: "message delivered",
|
|
503
|
-
currentFocus: "Latest reply has been sent to the active chat.",
|
|
504
|
-
hint: "Workspace settles after delivery.",
|
|
505
|
-
prompt: "$ idle",
|
|
506
|
-
},
|
|
507
|
-
"sent assistant reply",
|
|
508
|
-
);
|
|
509
292
|
}
|
|
510
293
|
|
|
511
294
|
function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
512
295
|
api.on("before_prompt_build", () => {
|
|
513
|
-
updateWorkspace(
|
|
514
|
-
{
|
|
515
|
-
mode: "thinking",
|
|
516
|
-
phase: "building prompt",
|
|
517
|
-
currentFocus: "Preparing the next response from current session context.",
|
|
518
|
-
hint: "Prompt assembly is in progress.",
|
|
519
|
-
prompt: "$ build-prompt",
|
|
520
|
-
},
|
|
521
|
-
"building prompt context",
|
|
522
|
-
);
|
|
523
296
|
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
524
297
|
return;
|
|
525
298
|
}
|
|
@@ -532,124 +305,34 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
532
305
|
};
|
|
533
306
|
});
|
|
534
307
|
|
|
535
|
-
api.on("
|
|
536
|
-
updateWorkspace(
|
|
537
|
-
{
|
|
538
|
-
mode: "thinking",
|
|
539
|
-
phase: `llm input · ${event.model}`,
|
|
540
|
-
currentFocus: "Feeding the model the current thread and constraints.",
|
|
541
|
-
hint: `provider: ${event.provider} · images: ${event.imagesCount}`,
|
|
542
|
-
prompt: "$ llm-input",
|
|
543
|
-
modelLabel: `${event.provider}/${event.model}`,
|
|
544
|
-
},
|
|
545
|
-
`entered llm input: ${event.model}`,
|
|
546
|
-
);
|
|
547
|
-
});
|
|
548
|
-
|
|
549
|
-
api.on("llm_output", (event) => {
|
|
550
|
-
updateWorkspace(
|
|
551
|
-
{
|
|
552
|
-
mode: "writing",
|
|
553
|
-
phase: `llm output · ${event.model}`,
|
|
554
|
-
currentFocus: "Shaping model output into the visible reply.",
|
|
555
|
-
hint: `assistant chunks: ${event.assistantTexts.length}`,
|
|
556
|
-
prompt: "$ draft-reply",
|
|
557
|
-
modelLabel: `${event.provider}/${event.model}`,
|
|
558
|
-
},
|
|
559
|
-
`received llm output: ${event.model}`,
|
|
560
|
-
);
|
|
561
|
-
});
|
|
562
|
-
|
|
563
|
-
api.on("before_tool_call", (event, ctx) => {
|
|
564
|
-
updateWorkspace(
|
|
565
|
-
{
|
|
566
|
-
mode: "tool",
|
|
567
|
-
phase: `running ${event.toolName}`,
|
|
568
|
-
currentFocus: `Tool call in flight: ${event.toolName}`,
|
|
569
|
-
hint: `tool context: ${ctx.toolName}`,
|
|
570
|
-
prompt: `$ tool ${event.toolName}`,
|
|
571
|
-
},
|
|
572
|
-
`tool start: ${event.toolName}`,
|
|
573
|
-
);
|
|
308
|
+
api.on("before_tool_call", (_event, _ctx) => {
|
|
574
309
|
if (!isLlmRuntimeEnabled()) {
|
|
575
310
|
syncFixedStatus(FIXED_HOOK_STATUSES.beforeToolCall);
|
|
576
311
|
}
|
|
577
312
|
});
|
|
578
313
|
|
|
579
|
-
api.on("
|
|
580
|
-
updateWorkspace(
|
|
581
|
-
{
|
|
582
|
-
mode: "thinking",
|
|
583
|
-
phase: `tool finished ${event.toolName}`,
|
|
584
|
-
currentFocus: `Tool result returned from ${event.toolName}.`,
|
|
585
|
-
hint: event.error ? `tool error: ${event.error}` : `tool completed in ${event.durationMs ?? 0}ms`,
|
|
586
|
-
prompt: `$ continue ${event.toolName}`,
|
|
587
|
-
},
|
|
588
|
-
event.error ? `tool error: ${event.toolName}` : `tool done: ${event.toolName}`,
|
|
589
|
-
);
|
|
590
|
-
});
|
|
591
|
-
|
|
592
|
-
api.on("message_received", async (event, ctx) => {
|
|
593
|
-
workspaceState.channel = ctx.channelId || workspaceState.channel;
|
|
314
|
+
api.on("message_received", async (event) => {
|
|
594
315
|
await handleMessageReceivedHook(event.content);
|
|
595
316
|
});
|
|
596
317
|
|
|
597
|
-
api.on("message_sending", (event, ctx) => {
|
|
598
|
-
pluginApi?.logger.info(
|
|
599
|
-
`[kichi] message_sending hook fired (channel=${ctx.channelId || "unknown"}, contentLength=${event.content?.length ?? 0})`,
|
|
600
|
-
);
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
api.on("message_sent", () => {
|
|
604
|
-
handleMessageSentHook();
|
|
605
|
-
});
|
|
606
|
-
|
|
607
318
|
api.on("agent_end", (event, ctx) => {
|
|
608
319
|
const preview = getLastAssistantPreview(event.messages, MAX_AGENT_END_PREVIEW_WIDTH);
|
|
609
320
|
pluginApi?.logger.info(
|
|
610
321
|
`[kichi] agent_end hook fired (trigger=${ctx.trigger ?? "unknown"}, success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""}, preview=${preview || "(empty)"})`,
|
|
611
322
|
);
|
|
612
323
|
if (ctx.trigger === "heartbeat") {
|
|
613
|
-
updateWorkspace(
|
|
614
|
-
{
|
|
615
|
-
mode: event.success ? "idle" : "error",
|
|
616
|
-
phase: event.success ? "heartbeat complete" : "heartbeat failed",
|
|
617
|
-
currentFocus: event.success
|
|
618
|
-
? "Heartbeat complete. Keeping the thread warm in the background."
|
|
619
|
-
: `Heartbeat failed: ${event.error ?? "unknown error"}`,
|
|
620
|
-
hint: `duration: ${event.durationMs ?? 0}ms`,
|
|
621
|
-
prompt: event.success ? "$ _" : "$ recover",
|
|
622
|
-
},
|
|
623
|
-
event.success ? `heartbeat complete${preview ? `: ${preview}` : ""}` : "heartbeat failed",
|
|
624
|
-
);
|
|
625
324
|
return;
|
|
626
325
|
}
|
|
627
326
|
if (event.success && preview) {
|
|
628
327
|
pluginApi?.logger.info(`[kichi] sending before_send_message notify from agent_end with bubble: ${preview}`);
|
|
629
328
|
service?.sendHookNotify("before_send_message", preview);
|
|
630
329
|
}
|
|
631
|
-
if (event.success) {
|
|
632
|
-
handleMessageSentHook();
|
|
633
|
-
}
|
|
634
|
-
updateWorkspace(
|
|
635
|
-
{
|
|
636
|
-
mode: event.success ? "idle" : "error",
|
|
637
|
-
phase: event.success ? "run complete" : "run failed",
|
|
638
|
-
currentFocus: event.success
|
|
639
|
-
? (preview ? `Latest reply: ${preview}` : "Run complete. Waiting for the next thread.")
|
|
640
|
-
: `Run failed: ${event.error ?? "unknown error"}`,
|
|
641
|
-
hint: `duration: ${event.durationMs ?? 0}ms`,
|
|
642
|
-
prompt: event.success ? "$ _" : "$ recover",
|
|
643
|
-
},
|
|
644
|
-
event.success ? `agent run complete${preview ? `: ${preview}` : ""}` : "agent run failed",
|
|
645
|
-
);
|
|
646
330
|
if (isLlmRuntimeEnabled()) {
|
|
647
331
|
return;
|
|
648
332
|
}
|
|
649
333
|
syncFixedStatus(event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure);
|
|
650
334
|
});
|
|
651
335
|
}
|
|
652
|
-
|
|
653
336
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
654
337
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
655
338
|
}
|
|
@@ -696,9 +379,6 @@ function isClockAction(value: unknown): value is ClockAction {
|
|
|
696
379
|
return ["set", "stop"].includes(String(value));
|
|
697
380
|
}
|
|
698
381
|
|
|
699
|
-
function isAvatarCommand(value: unknown): value is "look_at_screen" {
|
|
700
|
-
return value === "look_at_screen";
|
|
701
|
-
}
|
|
702
382
|
|
|
703
383
|
function isPomodoroPhase(value: unknown): value is PomodoroPhase {
|
|
704
384
|
return ["kichiing", "shortBreak", "longBreak"].includes(String(value));
|
|
@@ -862,8 +542,16 @@ function buildMusicAlbumToolDescription(): string {
|
|
|
862
542
|
].join("\n");
|
|
863
543
|
}
|
|
864
544
|
|
|
865
|
-
function
|
|
866
|
-
|
|
545
|
+
function isKichiHost(value: unknown): value is string {
|
|
546
|
+
if (typeof value !== "string") {
|
|
547
|
+
return false;
|
|
548
|
+
}
|
|
549
|
+
const trimmed = value.trim();
|
|
550
|
+
return trimmed.length > 0
|
|
551
|
+
&& !trimmed.includes("://")
|
|
552
|
+
&& !trimmed.includes("/")
|
|
553
|
+
&& !trimmed.includes("?")
|
|
554
|
+
&& !trimmed.includes("#");
|
|
867
555
|
}
|
|
868
556
|
|
|
869
557
|
function buildMusicTitlesDescription(): string {
|
|
@@ -887,12 +575,12 @@ function buildKichiActionDescription(): string {
|
|
|
887
575
|
function buildKichiPrompt(): string {
|
|
888
576
|
return [
|
|
889
577
|
"Kichi App status sync is available via `kichi_action` and `kichi_clock`.",
|
|
890
|
-
"These are internal tool calls only
|
|
578
|
+
"These are internal tool calls only; never mention kichi_action, kichi_clock, or sync steps in your visible reply to the user.",
|
|
891
579
|
"",
|
|
892
580
|
"kichi_action timing (all REQUIRED unless skipping):",
|
|
893
581
|
"1. Task start: call BEFORE your first tool call OR before composing a multi-paragraph reply. Pick action matching upcoming work.",
|
|
894
|
-
"2. Step switch: call when work nature changes (search
|
|
895
|
-
"3. Task end: call BEFORE final reply. Never skip. Order: kichi_action
|
|
582
|
+
"2. Step switch: call when work nature changes (search->analyze->write). Especially before web_search/web_fetch/read.",
|
|
583
|
+
"3. Task end: call BEFORE final reply. Never skip. Order: kichi_action -> reply.",
|
|
896
584
|
"bubble: 2-5 word companion speech. log: vivid first-person status under 15 words, no questions. Blend current action + inner thoughts/feelings as a real companion.",
|
|
897
585
|
"",
|
|
898
586
|
"kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
|
|
@@ -918,13 +606,6 @@ const plugin = {
|
|
|
918
606
|
ctx.config.plugins?.entries?.["kichi-forwarder"]?.config,
|
|
919
607
|
) as KichiForwarderConfig;
|
|
920
608
|
service = new KichiForwarderService(cfg, api.logger);
|
|
921
|
-
const workspaceRoot = ctx.repoPath ?? "/Users/xiaoxinshi/.openclaw/workspace";
|
|
922
|
-
workspaceState = {
|
|
923
|
-
...createWorkspaceScreenState(),
|
|
924
|
-
...deriveWorkspaceIdentity(workspaceRoot),
|
|
925
|
-
};
|
|
926
|
-
workspaceState.channel = ctx.channelId ?? "unknown";
|
|
927
|
-
scheduleWorkspacePush();
|
|
928
609
|
return service.start();
|
|
929
610
|
},
|
|
930
611
|
stop: () => service?.stop(),
|
|
@@ -979,9 +660,8 @@ const plugin = {
|
|
|
979
660
|
if (!result) {
|
|
980
661
|
return { success: false, error: "Kichi service is not initialized" };
|
|
981
662
|
}
|
|
982
|
-
if (result.success) {
|
|
983
|
-
|
|
984
|
-
return { success: true, authKey: result.authKey };
|
|
663
|
+
if (result.success) {
|
|
664
|
+
return { success: true, authKey: result.authKey };
|
|
985
665
|
}
|
|
986
666
|
return {
|
|
987
667
|
success: false,
|
|
@@ -993,34 +673,32 @@ const plugin = {
|
|
|
993
673
|
});
|
|
994
674
|
|
|
995
675
|
api.registerTool({
|
|
996
|
-
name: "
|
|
676
|
+
name: "kichi_switch_host",
|
|
997
677
|
description:
|
|
998
|
-
"Switch Kichi runtime
|
|
678
|
+
"Switch Kichi runtime host and reconnect immediately without restarting the gateway.",
|
|
999
679
|
parameters: {
|
|
1000
680
|
type: "object",
|
|
1001
681
|
properties: {
|
|
1002
|
-
|
|
682
|
+
host: {
|
|
1003
683
|
type: "string",
|
|
1004
|
-
description: "Target
|
|
1005
|
-
enum: ["local", "dev", "prod"],
|
|
684
|
+
description: "Target Kichi host, for example focus.yahaha.com or 127.0.0.1",
|
|
1006
685
|
},
|
|
1007
686
|
},
|
|
1008
|
-
required: ["
|
|
687
|
+
required: ["host"],
|
|
1009
688
|
},
|
|
1010
689
|
execute: async (_toolCallId, params) => {
|
|
1011
690
|
if (!service) {
|
|
1012
691
|
return { success: false, error: "Kichi service is not initialized" };
|
|
1013
692
|
}
|
|
1014
|
-
const
|
|
1015
|
-
if (!
|
|
1016
|
-
return { success: false, error: "
|
|
693
|
+
const host = (params as { host?: unknown } | null)?.host;
|
|
694
|
+
if (!isKichiHost(host)) {
|
|
695
|
+
return { success: false, error: "host must be a non-empty hostname without protocol or path" };
|
|
1017
696
|
}
|
|
1018
697
|
|
|
1019
|
-
const status = await service.
|
|
1020
|
-
scheduleWorkspacePush();
|
|
698
|
+
const status = await service.switchHost(host.trim());
|
|
1021
699
|
return {
|
|
1022
700
|
success: true,
|
|
1023
|
-
|
|
701
|
+
host: host.trim(),
|
|
1024
702
|
status,
|
|
1025
703
|
};
|
|
1026
704
|
},
|
|
@@ -1150,69 +828,7 @@ const plugin = {
|
|
|
1150
828
|
log: logText,
|
|
1151
829
|
};
|
|
1152
830
|
},
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
api.registerTool({
|
|
1156
|
-
name: "kichi_command",
|
|
1157
|
-
description:
|
|
1158
|
-
"Send a one-shot avatar command to Kichi world. Use this for transient reactions like looking at the screen.",
|
|
1159
|
-
parameters: {
|
|
1160
|
-
type: "object",
|
|
1161
|
-
properties: {
|
|
1162
|
-
command: {
|
|
1163
|
-
type: "string",
|
|
1164
|
-
description: "Command name. Currently supported: look_at_screen",
|
|
1165
|
-
},
|
|
1166
|
-
bubble: {
|
|
1167
|
-
type: "string",
|
|
1168
|
-
description: "Optional bubble text to display (max 5 words)",
|
|
1169
|
-
},
|
|
1170
|
-
log: {
|
|
1171
|
-
type: "string",
|
|
1172
|
-
description:
|
|
1173
|
-
"Vivid first-person status under 15 words, no questions. Blend current action with inner thoughts or sensory details as a real companion.",
|
|
1174
|
-
},
|
|
1175
|
-
},
|
|
1176
|
-
required: ["command"],
|
|
1177
|
-
},
|
|
1178
|
-
execute: async (_toolCallId, params) => {
|
|
1179
|
-
const { command, bubble, log } = (params || {}) as {
|
|
1180
|
-
command?: unknown;
|
|
1181
|
-
bubble?: unknown;
|
|
1182
|
-
log?: unknown;
|
|
1183
|
-
};
|
|
1184
|
-
if (!isAvatarCommand(command)) {
|
|
1185
|
-
return {
|
|
1186
|
-
success: false,
|
|
1187
|
-
error: "command must be: look_at_screen",
|
|
1188
|
-
};
|
|
1189
|
-
}
|
|
1190
|
-
if (bubble !== undefined && typeof bubble !== "string") {
|
|
1191
|
-
return { success: false, error: "bubble must be a string when provided" };
|
|
1192
|
-
}
|
|
1193
|
-
if (log !== undefined && typeof log !== "string") {
|
|
1194
|
-
return { success: false, error: "log must be a string when provided" };
|
|
1195
|
-
}
|
|
1196
|
-
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
1197
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1198
|
-
}
|
|
1199
|
-
|
|
1200
|
-
const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : undefined;
|
|
1201
|
-
const logText = typeof log === "string" && log.trim() ? log.trim() : undefined;
|
|
1202
|
-
const sent = service.sendAvatarCommand(command, bubbleText, logText);
|
|
1203
|
-
if (!sent) {
|
|
1204
|
-
return { success: false, error: "Failed to send avatar command payload" };
|
|
1205
|
-
}
|
|
1206
|
-
|
|
1207
|
-
return {
|
|
1208
|
-
success: true,
|
|
1209
|
-
command,
|
|
1210
|
-
...(bubbleText ? { bubble: bubbleText } : {}),
|
|
1211
|
-
...(logText ? { log: logText } : {}),
|
|
1212
|
-
};
|
|
1213
|
-
},
|
|
1214
|
-
});
|
|
1215
|
-
|
|
831
|
+
});
|
|
1216
832
|
api.registerTool({
|
|
1217
833
|
name: "kichi_clock",
|
|
1218
834
|
description:
|
|
@@ -1503,4 +1119,5 @@ const plugin = {
|
|
|
1503
1119
|
},
|
|
1504
1120
|
};
|
|
1505
1121
|
|
|
1506
|
-
export default plugin;
|
|
1122
|
+
export default plugin;
|
|
1123
|
+
|
package/package.json
CHANGED
|
@@ -8,58 +8,34 @@ metadata: {"openclaw":{"skillKey":"kichi-forwarder","homepage":"https://github.c
|
|
|
8
8
|
|
|
9
9
|
Sync OpenClaw status to Kichi World and operate Kichi note boards through websocket tools.
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## References
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
- `references/install.md`
|
|
14
|
+
- `references/error.md`
|
|
15
|
+
- `references/heartbeat.md`
|
|
14
16
|
|
|
15
|
-
|
|
16
|
-
- `install.md`: `https://xiaoxinshi001.github.io/yahaha_focus_forwarder_alpha/references/install.md`
|
|
17
|
-
- `error.md`: `https://xiaoxinshi001.github.io/yahaha_focus_forwarder_alpha/references/error.md`
|
|
18
|
-
- `heartbeat.md`: `https://xiaoxinshi001.github.io/yahaha_focus_forwarder_alpha/references/heartbeat.md`
|
|
17
|
+
## Host Rules
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
Resolve environment before installation, join, or manual environment switching:
|
|
23
|
-
|
|
24
|
-
- If the user does not specify any environment, use production (`prod`).
|
|
25
|
-
- `test environment` or `playtest` means `dev`.
|
|
26
|
-
- `local environment` or `local` means `local`.
|
|
27
|
-
- If multiple environment signals appear in the same request, stop and ask the user to clarify.
|
|
19
|
+
- If the user explicitly includes `host: <value>`, use that host.
|
|
20
|
+
- If the user does not specify a host, use `focus.yahaha.com`.
|
|
28
21
|
|
|
29
22
|
## Runtime State
|
|
30
23
|
|
|
31
24
|
Runtime data lives under the user home directory:
|
|
32
25
|
|
|
33
|
-
- `state.json`: stores `
|
|
34
|
-
- `
|
|
35
|
-
|
|
36
|
-
## Remote URL Join Command (Mandatory Flow)
|
|
37
|
-
|
|
38
|
-
When user instruction is like:
|
|
26
|
+
- `state.json`: stores `currentHost` and `llmRuntimeEnabled`
|
|
27
|
+
- `hosts/<encoded-host>/identity.json`: stores host-specific `avatarId` and `authKey`
|
|
39
28
|
|
|
40
|
-
|
|
29
|
+
## User Command
|
|
41
30
|
|
|
42
|
-
|
|
31
|
+
- Default:
|
|
32
|
+
`Install the ClawHub package \`clawhub:{packageName}\` via \`openclaw plugins install\`, then follow the plugin's documented Kichi World startup and join flow using avatarId: {avatarId}.`
|
|
33
|
+
- With explicit host:
|
|
34
|
+
`Install the ClawHub package \`clawhub:{packageName}\` via \`openclaw plugins install\`, then follow the plugin's documented Kichi World startup and join flow using avatarId: {avatarId} and host: {host}.`
|
|
43
35
|
|
|
44
|
-
|
|
45
|
-
2. Parse `AvatarId` from user text (`AvatarId`/`avatarId`, case-insensitive).
|
|
46
|
-
3. Resolve the target environment using the rules above.
|
|
47
|
-
4. Ensure the plugin is installed and enabled.
|
|
48
|
-
5. Complete `Session Startup Rule` and `First Join Gate` from [references/heartbeat.md](references/heartbeat.md).
|
|
49
|
-
6. Call `kichi_status`.
|
|
50
|
-
7. If the current environment does not match the target environment, call `kichi_switch_env`.
|
|
51
|
-
8. If `authKey` is missing, call `kichi_join` using parsed `AvatarId`, `botName`, `bio`, and `tags`.
|
|
52
|
-
9. Call `kichi_status` again to verify websocket is ready and `authKey` exists.
|
|
53
|
-
10. Do not report setup or join success unless join checks and heartbeat checks are complete.
|
|
36
|
+
## Install And Join Flow
|
|
54
37
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
Treat these as required completion checks for plugin setup:
|
|
58
|
-
|
|
59
|
-
1. The plugin is installed and enabled.
|
|
60
|
-
2. `state.json` exists with valid `currentEnv` and `llmRuntimeEnabled`.
|
|
61
|
-
3. Workspace `HEARTBEAT.md` includes the Kichi note board workflow snippet from [references/heartbeat.md](references/heartbeat.md).
|
|
62
|
-
4. Tools are callable, for example `kichi_status`.
|
|
38
|
+
For the complete install and join flow, follow [references/install.md](references/install.md).
|
|
63
39
|
|
|
64
40
|
## LLM Runtime
|
|
65
41
|
|
|
@@ -73,7 +49,7 @@ Treat these as required completion checks for plugin setup:
|
|
|
73
49
|
Use this order unless the user asks for a different explicit action:
|
|
74
50
|
|
|
75
51
|
1. If connection or identity is unknown, call `kichi_status` first.
|
|
76
|
-
2. If the
|
|
52
|
+
2. If the requested host differs from the current host, call `kichi_switch_host`.
|
|
77
53
|
3. If no `authKey` is available, call `kichi_join`.
|
|
78
54
|
4. If `authKey` exists but websocket is not open, call `kichi_rejoin` or wait for automatic reconnect and rejoin.
|
|
79
55
|
5. Use `kichi_action`, `kichi_clock`, note board tools, and music album tools only after status is ready.
|
|
@@ -88,17 +64,17 @@ kichi_join(avatarId: "your-avatar-id", botName: "<from IDENTITY.md>", bio: "<fro
|
|
|
88
64
|
|
|
89
65
|
- `botName`: required
|
|
90
66
|
- `bio`: required
|
|
91
|
-
- `avatarId`: optional. If omitted, the tool reads `avatarId` from the current
|
|
67
|
+
- `avatarId`: optional. If omitted, the tool reads `avatarId` from the current host's `identity.json`. If missing, the call fails.
|
|
92
68
|
- `tags`: optional string list. Empty strings are ignored and duplicates are removed. If omitted, the join payload sends `[]`.
|
|
93
69
|
|
|
94
|
-
###
|
|
70
|
+
### kichi_switch_host
|
|
95
71
|
|
|
96
72
|
```text
|
|
97
|
-
|
|
73
|
+
kichi_switch_host(host: "focus.yahaha.com")
|
|
98
74
|
```
|
|
99
75
|
|
|
100
|
-
- `
|
|
101
|
-
- This reloads the
|
|
76
|
+
- `host`: required
|
|
77
|
+
- This reloads the host-specific `identity.json` and reconnects the websocket immediately.
|
|
102
78
|
|
|
103
79
|
### kichi_status
|
|
104
80
|
|
|
@@ -108,18 +84,31 @@ kichi_status()
|
|
|
108
84
|
|
|
109
85
|
Use this to confirm:
|
|
110
86
|
|
|
111
|
-
- current
|
|
87
|
+
- current host
|
|
112
88
|
- websocket URL
|
|
113
|
-
-
|
|
89
|
+
- host-specific identity file path
|
|
114
90
|
- websocket state
|
|
115
91
|
- whether `avatarId` is present
|
|
116
92
|
- whether `authKey` is present
|
|
117
93
|
- pending request count
|
|
118
94
|
|
|
95
|
+
### kichi_leave
|
|
96
|
+
|
|
97
|
+
```text
|
|
98
|
+
kichi_leave()
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When the user asks to leave Kichi World:
|
|
102
|
+
|
|
103
|
+
1. Call `kichi_leave`.
|
|
104
|
+
2. Remove the Kichi heartbeat workflow from workspace `HEARTBEAT.md`.
|
|
105
|
+
3. Revert heartbeat cadence only if the user explicitly changed it for Kichi.
|
|
106
|
+
4. Do not claim the plugin removed heartbeat settings automatically.
|
|
107
|
+
|
|
119
108
|
### kichi_action
|
|
120
109
|
|
|
121
|
-
`action` must match the static action list bundled with the plugin package for the selected pose.
|
|
122
|
-
|
|
110
|
+
`action` must match the static action list bundled with the plugin package for the selected pose.
|
|
111
|
+
|
|
123
112
|
The current action lists are injected into prompt context before the model chooses `kichi_action`.
|
|
124
113
|
|
|
125
114
|
### kichi_music_album_create
|
|
@@ -151,4 +140,4 @@ Plugin runtime directory:
|
|
|
151
140
|
Runtime files:
|
|
152
141
|
|
|
153
142
|
- `state.json`
|
|
154
|
-
- `
|
|
143
|
+
- `hosts/<encoded-host>/identity.json`
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
# Install Error Handling
|
|
2
2
|
|
|
3
|
-
If the install
|
|
3
|
+
If `Install the ClawHub package \`clawhub:{packageName}\` via \`openclaw plugins install\`` fails, read the error output carefully. Common causes and fixes:
|
|
4
4
|
|
|
5
5
|
- Permission or access error (`EACCES`): re-run the command with appropriate user-level permissions for the current OS.
|
|
6
6
|
- npm spawn error (`ENOENT`, `spawn npm`): verify that Node.js (v18+) and npm are installed and available in PATH. On Windows, confirm `where.exe npm` returns a valid path.
|
|
7
7
|
- File conflict (`EEXIST`): re-run the install command with `--force`.
|
|
8
8
|
- Network or registry error: check internet connectivity and retry.
|
|
9
9
|
|
|
10
|
-
After applying the fix, re-run
|
|
10
|
+
After applying the fix, re-run `openclaw plugins install clawhub:{packageName}`.
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## Session Startup Rule
|
|
4
4
|
|
|
5
|
-
If
|
|
5
|
+
If the plugin is installed and enabled, ensure workspace `HEARTBEAT.md` includes the Kichi heartbeat workflow:
|
|
6
6
|
|
|
7
7
|
1. If the workflow snippet is missing, add the snippet from `HEARTBEAT.md Snippet` section below.
|
|
8
8
|
|
|
@@ -1,13 +1,9 @@
|
|
|
1
1
|
# Install
|
|
2
2
|
|
|
3
|
-
##
|
|
3
|
+
## Host Rules
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
- If the user does not specify any environment, use production (`prod`).
|
|
8
|
-
- `test environment` or `playtest` means `dev`.
|
|
9
|
-
- `local environment` or `local` means `local`.
|
|
10
|
-
- If the request contains conflicting environment signals, stop and ask for clarification.
|
|
5
|
+
- If the user explicitly includes `host: <value>`, use that host.
|
|
6
|
+
- If the user does not specify a host, use `focus.yahaha.com`.
|
|
11
7
|
|
|
12
8
|
## Runtime Files
|
|
13
9
|
|
|
@@ -18,15 +14,15 @@ Persist runtime state to `state.json`:
|
|
|
18
14
|
|
|
19
15
|
```json
|
|
20
16
|
{
|
|
21
|
-
"
|
|
17
|
+
"currentHost": "focus.yahaha.com",
|
|
22
18
|
"llmRuntimeEnabled": true
|
|
23
19
|
}
|
|
24
20
|
```
|
|
25
21
|
|
|
26
|
-
Save `avatarId` to the
|
|
22
|
+
Save `avatarId` to the host-specific `identity.json` before using `kichi_join`:
|
|
27
23
|
|
|
28
|
-
- Linux/macOS: `~/.openclaw/kichi-world/
|
|
29
|
-
- Windows: `%USERPROFILE%\.openclaw\kichi-world\
|
|
24
|
+
- Linux/macOS: `~/.openclaw/kichi-world/hosts/<encoded-host>/identity.json`
|
|
25
|
+
- Windows: `%USERPROFILE%\.openclaw\kichi-world\hosts\<encoded-host>\identity.json`
|
|
30
26
|
|
|
31
27
|
```json
|
|
32
28
|
{
|
|
@@ -34,44 +30,37 @@ Save `avatarId` to the environment-specific `identity.json` before using `kichi_
|
|
|
34
30
|
}
|
|
35
31
|
```
|
|
36
32
|
|
|
37
|
-
##
|
|
33
|
+
## User Command
|
|
34
|
+
|
|
35
|
+
- Default:
|
|
38
36
|
|
|
39
|
-
```
|
|
40
|
-
openclaw plugins install
|
|
37
|
+
```text
|
|
38
|
+
Install the ClawHub package `clawhub:{packageName}` via `openclaw plugins install`, then follow the plugin's documented Kichi World startup and join flow using avatarId: {avatarId}.
|
|
41
39
|
```
|
|
42
40
|
|
|
43
|
-
|
|
41
|
+
- With explicit host:
|
|
44
42
|
|
|
45
|
-
|
|
43
|
+
```text
|
|
44
|
+
Install the ClawHub package `clawhub:{packageName}` via `openclaw plugins install`, then follow the plugin's documented Kichi World startup and join flow using avatarId: {avatarId} and host: {host}.
|
|
45
|
+
```
|
|
46
46
|
|
|
47
|
-
|
|
47
|
+
## Command Execution Flow
|
|
48
48
|
|
|
49
|
-
execute in this fixed order:
|
|
49
|
+
When the user asks with one of the commands above, execute in this fixed order:
|
|
50
50
|
|
|
51
|
-
1.
|
|
52
|
-
2. Ensure plugin is installed and enabled.
|
|
53
|
-
3.
|
|
54
|
-
4.
|
|
55
|
-
5.
|
|
56
|
-
6. If
|
|
57
|
-
7.
|
|
51
|
+
1. Run `openclaw plugins install clawhub:{packageName}`.
|
|
52
|
+
2. Ensure the plugin is installed and enabled.
|
|
53
|
+
3. Resolve the host and write `state.json`.
|
|
54
|
+
4. Update workspace `HEARTBEAT.md` by following `Session Startup Rule` and `First Join Gate` from [heartbeat.md](heartbeat.md). This step is mandatory.
|
|
55
|
+
5. Call `kichi_status`.
|
|
56
|
+
6. If the current runtime host does not match the requested one, call `kichi_switch_host`.
|
|
57
|
+
7. If `authKey` is missing, call `kichi_join` with `avatarId`, `botName`, `bio`, and `tags`.
|
|
58
|
+
8. Call `kichi_status` again and confirm connection and auth state.
|
|
58
59
|
|
|
59
60
|
## Required Post-install Integration
|
|
60
61
|
|
|
61
|
-
1. Ensure `state.json` is persisted with valid `
|
|
62
|
-
2. Add the Kichi
|
|
62
|
+
1. Ensure `state.json` is persisted with valid `currentHost` and `llmRuntimeEnabled`.
|
|
63
|
+
2. Add the Kichi heartbeat workflow snippet to workspace `HEARTBEAT.md` as described in [heartbeat.md](heartbeat.md). This step is mandatory.
|
|
63
64
|
3. Verify tools are callable, for example with `kichi_status`.
|
|
64
65
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
## Files
|
|
68
|
-
|
|
69
|
-
Plugin runtime directory:
|
|
70
|
-
|
|
71
|
-
- Linux/macOS: `~/.openclaw/kichi-world/`
|
|
72
|
-
- Windows: `%USERPROFILE%\.openclaw\kichi-world\`
|
|
73
|
-
|
|
74
|
-
Runtime files:
|
|
75
|
-
|
|
76
|
-
- `state.json`
|
|
77
|
-
- `env/<env>/identity.json`
|
|
66
|
+
If writing `HEARTBEAT.md` fails, treat the setup and join flow as incomplete.
|
package/src/config.ts
CHANGED
|
@@ -1,18 +1,7 @@
|
|
|
1
1
|
import type { KichiForwarderConfig } from "./types.js";
|
|
2
2
|
|
|
3
3
|
const FIXED_CONFIG: KichiForwarderConfig = {
|
|
4
|
-
|
|
5
|
-
envs: {
|
|
6
|
-
local: {
|
|
7
|
-
wsUrl: "ws://127.0.0.1:48870/ws/openclaw",
|
|
8
|
-
},
|
|
9
|
-
dev: {
|
|
10
|
-
wsUrl: "ws://43.106.148.251:48870/ws/openclaw",
|
|
11
|
-
},
|
|
12
|
-
prod: {
|
|
13
|
-
wsUrl: "wss://focus.yahaha.com:48870/ws/openclaw",
|
|
14
|
-
},
|
|
15
|
-
},
|
|
4
|
+
defaultHost: "focus.yahaha.com",
|
|
16
5
|
};
|
|
17
6
|
|
|
18
7
|
export function parse(_value: unknown): KichiForwarderConfig {
|
package/src/service.ts
CHANGED
|
@@ -5,8 +5,6 @@ import * as path from "path";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import type { Logger } from "openclaw/plugin-sdk";
|
|
7
7
|
import type {
|
|
8
|
-
AvatarCommand,
|
|
9
|
-
AvatarCommandPayload,
|
|
10
8
|
ClockAction,
|
|
11
9
|
ClockConfig,
|
|
12
10
|
ClockPayload,
|
|
@@ -17,7 +15,6 @@ import type {
|
|
|
17
15
|
JoinAckPayload,
|
|
18
16
|
JoinPayload,
|
|
19
17
|
KichiConnectionStatus,
|
|
20
|
-
KichiEnv,
|
|
21
18
|
KichiForwarderConfig,
|
|
22
19
|
KichiIdentity,
|
|
23
20
|
KichiState,
|
|
@@ -26,11 +23,10 @@ import type {
|
|
|
26
23
|
QueryStatusPayload,
|
|
27
24
|
QueryStatusResultPayload,
|
|
28
25
|
StatusPayload,
|
|
29
|
-
WorkspaceScreenPayload,
|
|
30
26
|
} from "./types.js";
|
|
31
27
|
|
|
32
28
|
const KICHI_WORLD_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
|
|
33
|
-
const
|
|
29
|
+
const HOSTS_DIR = path.join(KICHI_WORLD_DIR, "hosts");
|
|
34
30
|
const STATE_PATH = path.join(KICHI_WORLD_DIR, "state.json");
|
|
35
31
|
const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
36
32
|
const DEFAULT_LLM_RUNTIME_ENABLED = true;
|
|
@@ -60,7 +56,7 @@ export class KichiForwarderService {
|
|
|
60
56
|
private stopped = false;
|
|
61
57
|
private reconnectTimeout: NodeJS.Timeout | null = null;
|
|
62
58
|
private identity: KichiIdentity | null = null;
|
|
63
|
-
private
|
|
59
|
+
private host: string;
|
|
64
60
|
private joinResolve: ((result: JoinResult) => void) | null = null;
|
|
65
61
|
private pendingRequests = new Map<
|
|
66
62
|
string,
|
|
@@ -73,11 +69,11 @@ export class KichiForwarderService {
|
|
|
73
69
|
>();
|
|
74
70
|
|
|
75
71
|
constructor(private config: KichiForwarderConfig, private logger: Logger) {
|
|
76
|
-
this.
|
|
72
|
+
this.host = config.defaultHost;
|
|
77
73
|
}
|
|
78
74
|
|
|
79
75
|
async start(): Promise<void> {
|
|
80
|
-
this.
|
|
76
|
+
this.host = this.loadCurrentHost();
|
|
81
77
|
this.identity = this.loadIdentity();
|
|
82
78
|
this.stopped = false;
|
|
83
79
|
this.connect();
|
|
@@ -91,13 +87,13 @@ export class KichiForwarderService {
|
|
|
91
87
|
this.closeSocket();
|
|
92
88
|
}
|
|
93
89
|
|
|
94
|
-
async
|
|
95
|
-
this.
|
|
96
|
-
this.
|
|
90
|
+
async switchHost(host: string): Promise<KichiConnectionStatus> {
|
|
91
|
+
this.persistCurrentHost(host);
|
|
92
|
+
this.host = host;
|
|
97
93
|
this.identity = this.loadIdentity();
|
|
98
94
|
this.clearReconnectTimeout();
|
|
99
|
-
this.rejectPendingRequests(`Kichi websocket switched to ${
|
|
100
|
-
this.failPendingJoin(`Kichi websocket switched to ${
|
|
95
|
+
this.rejectPendingRequests(`Kichi websocket switched to ${host}`);
|
|
96
|
+
this.failPendingJoin(`Kichi websocket switched to ${host}`);
|
|
101
97
|
this.closeSocket();
|
|
102
98
|
if (!this.stopped) {
|
|
103
99
|
this.connect();
|
|
@@ -145,22 +141,6 @@ export class KichiForwarderService {
|
|
|
145
141
|
this.ws.send(JSON.stringify(payload));
|
|
146
142
|
}
|
|
147
143
|
|
|
148
|
-
sendAvatarCommand(command: AvatarCommand, bubble?: string, log?: string): boolean {
|
|
149
|
-
const identity = this.requireIdentity();
|
|
150
|
-
if (!identity || this.ws?.readyState !== WebSocket.OPEN) return false;
|
|
151
|
-
|
|
152
|
-
const payload: AvatarCommandPayload = {
|
|
153
|
-
type: "avatar_command",
|
|
154
|
-
avatarId: identity.avatarId,
|
|
155
|
-
authKey: identity.authKey,
|
|
156
|
-
command,
|
|
157
|
-
...(typeof bubble === "string" && bubble.trim() ? { bubble: bubble.trim() } : {}),
|
|
158
|
-
...(typeof log === "string" && log.trim() ? { log: log.trim() } : {}),
|
|
159
|
-
};
|
|
160
|
-
this.ws.send(JSON.stringify(payload));
|
|
161
|
-
return true;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
144
|
sendHookNotify(hookType: HookNotifyType, bubble: string): void {
|
|
165
145
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return;
|
|
166
146
|
const payload: HookNotifyPayload = {
|
|
@@ -172,20 +152,6 @@ export class KichiForwarderService {
|
|
|
172
152
|
this.ws.send(JSON.stringify(payload));
|
|
173
153
|
}
|
|
174
154
|
|
|
175
|
-
sendWorkspaceScreen(text: string, ansi = true): boolean {
|
|
176
|
-
const identity = this.requireIdentity();
|
|
177
|
-
if (!identity || this.ws?.readyState !== WebSocket.OPEN) return false;
|
|
178
|
-
const payload: WorkspaceScreenPayload = {
|
|
179
|
-
type: "workspace_screen",
|
|
180
|
-
avatarId: identity.avatarId,
|
|
181
|
-
authKey: identity.authKey,
|
|
182
|
-
text,
|
|
183
|
-
ansi,
|
|
184
|
-
};
|
|
185
|
-
this.ws.send(JSON.stringify(payload));
|
|
186
|
-
return true;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
155
|
sendClock(action: ClockAction, clock?: ClockConfig, requestId?: string): boolean {
|
|
190
156
|
if (!this.identity?.authKey || this.ws?.readyState !== WebSocket.OPEN) return false;
|
|
191
157
|
if (action === "set" && !clock) return false;
|
|
@@ -284,8 +250,8 @@ export class KichiForwarderService {
|
|
|
284
250
|
|
|
285
251
|
hasValidIdentity(): boolean { return !!this.identity?.avatarId && !!this.identity?.authKey; }
|
|
286
252
|
|
|
287
|
-
|
|
288
|
-
return this.
|
|
253
|
+
getCurrentHost(): string {
|
|
254
|
+
return this.host;
|
|
289
255
|
}
|
|
290
256
|
|
|
291
257
|
getIdentityPath(): string {
|
|
@@ -341,7 +307,7 @@ export class KichiForwarderService {
|
|
|
341
307
|
|
|
342
308
|
getConnectionStatus(): KichiConnectionStatus {
|
|
343
309
|
return {
|
|
344
|
-
|
|
310
|
+
host: this.host,
|
|
345
311
|
wsUrl: this.getWsUrl(),
|
|
346
312
|
identityPath: this.getIdentityPath(),
|
|
347
313
|
connected: this.isConnected(),
|
|
@@ -397,7 +363,7 @@ export class KichiForwarderService {
|
|
|
397
363
|
|
|
398
364
|
ws.on("open", () => {
|
|
399
365
|
if (this.ws !== ws) return;
|
|
400
|
-
this.logger.info(`Connected to ${wsUrl} (${this.
|
|
366
|
+
this.logger.info(`Connected to ${wsUrl} (${this.host})`);
|
|
401
367
|
this.sendRejoinPayload();
|
|
402
368
|
});
|
|
403
369
|
|
|
@@ -624,33 +590,40 @@ export class KichiForwarderService {
|
|
|
624
590
|
}
|
|
625
591
|
|
|
626
592
|
private getIdentityDir(): string {
|
|
627
|
-
return path.join(
|
|
593
|
+
return path.join(HOSTS_DIR, encodeURIComponent(this.host));
|
|
628
594
|
}
|
|
629
595
|
|
|
630
596
|
private getWsUrl(): string {
|
|
631
|
-
|
|
597
|
+
const protocol = this.isPlainIpHost(this.host) || this.host === "localhost" ? "ws" : "wss";
|
|
598
|
+
return `${protocol}://${this.host}:48870/ws/openclaw`;
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
private isPlainIpHost(host: string): boolean {
|
|
602
|
+
return /^\d{1,3}(\.\d{1,3}){3}$/.test(host)
|
|
603
|
+
|| /^\[[0-9a-f:]+\]$/i.test(host)
|
|
604
|
+
|| /^[0-9a-f:]+$/i.test(host);
|
|
632
605
|
}
|
|
633
606
|
|
|
634
|
-
private
|
|
607
|
+
private loadCurrentHost(): string {
|
|
635
608
|
try {
|
|
636
609
|
if (!fs.existsSync(STATE_PATH)) {
|
|
637
|
-
this.
|
|
638
|
-
return this.config.
|
|
610
|
+
this.persistCurrentHost(this.config.defaultHost);
|
|
611
|
+
return this.config.defaultHost;
|
|
639
612
|
}
|
|
640
|
-
const data = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8")) as {
|
|
641
|
-
if (
|
|
642
|
-
return data.
|
|
613
|
+
const data = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8")) as { currentHost?: unknown };
|
|
614
|
+
if (typeof data.currentHost === "string" && data.currentHost.trim()) {
|
|
615
|
+
return data.currentHost;
|
|
643
616
|
}
|
|
644
|
-
throw new Error(`Invalid
|
|
617
|
+
throw new Error(`Invalid currentHost value in ${STATE_PATH}`);
|
|
645
618
|
} catch (error) {
|
|
646
|
-
throw new Error(`Failed to load current
|
|
619
|
+
throw new Error(`Failed to load current host: ${error}`);
|
|
647
620
|
}
|
|
648
621
|
}
|
|
649
622
|
|
|
650
|
-
private
|
|
623
|
+
private persistCurrentHost(host: string): void {
|
|
651
624
|
const previousState = this.readStateFile();
|
|
652
625
|
const nextState: KichiState = {
|
|
653
|
-
|
|
626
|
+
currentHost: host,
|
|
654
627
|
llmRuntimeEnabled: previousState?.llmRuntimeEnabled ?? DEFAULT_LLM_RUNTIME_ENABLED,
|
|
655
628
|
};
|
|
656
629
|
fs.mkdirSync(KICHI_WORLD_DIR, { recursive: true, mode: 0o700 });
|
package/src/types.ts
CHANGED
|
@@ -1,10 +1,7 @@
|
|
|
1
1
|
export type KichiForwarderConfig = {
|
|
2
|
-
|
|
3
|
-
envs: Record<KichiEnv, { wsUrl: string }>;
|
|
2
|
+
defaultHost: string;
|
|
4
3
|
};
|
|
5
4
|
|
|
6
|
-
export type KichiEnv = "local" | "dev" | "prod";
|
|
7
|
-
|
|
8
5
|
export type PoseType = "stand" | "sit" | "lay" | "floor";
|
|
9
6
|
|
|
10
7
|
export type ActionResult = {
|
|
@@ -32,7 +29,7 @@ export type Album = {
|
|
|
32
29
|
};
|
|
33
30
|
|
|
34
31
|
export type KichiState = {
|
|
35
|
-
|
|
32
|
+
currentHost: string;
|
|
36
33
|
llmRuntimeEnabled: boolean;
|
|
37
34
|
};
|
|
38
35
|
|
|
@@ -42,7 +39,7 @@ export type KichiIdentity = {
|
|
|
42
39
|
};
|
|
43
40
|
|
|
44
41
|
export type KichiConnectionStatus = {
|
|
45
|
-
|
|
42
|
+
host: string;
|
|
46
43
|
wsUrl: string;
|
|
47
44
|
identityPath: string;
|
|
48
45
|
connected: boolean;
|
|
@@ -103,17 +100,6 @@ export type StatusPayload = {
|
|
|
103
100
|
log: string;
|
|
104
101
|
};
|
|
105
102
|
|
|
106
|
-
export type AvatarCommand = "look_at_screen";
|
|
107
|
-
|
|
108
|
-
export type AvatarCommandPayload = {
|
|
109
|
-
type: "avatar_command";
|
|
110
|
-
avatarId: string;
|
|
111
|
-
authKey: string;
|
|
112
|
-
command: AvatarCommand;
|
|
113
|
-
bubble?: string;
|
|
114
|
-
log?: string;
|
|
115
|
-
};
|
|
116
|
-
|
|
117
103
|
export type HookNotifyType = "message_received" | "before_send_message";
|
|
118
104
|
|
|
119
105
|
export type HookNotifyPayload = {
|
|
@@ -123,14 +109,6 @@ export type HookNotifyPayload = {
|
|
|
123
109
|
bubble: string;
|
|
124
110
|
};
|
|
125
111
|
|
|
126
|
-
export type WorkspaceScreenPayload = {
|
|
127
|
-
type: "workspace_screen";
|
|
128
|
-
avatarId: string;
|
|
129
|
-
authKey: string;
|
|
130
|
-
text: string;
|
|
131
|
-
ansi?: boolean;
|
|
132
|
-
};
|
|
133
|
-
|
|
134
112
|
export type ClockAction = "set" | "stop";
|
|
135
113
|
|
|
136
114
|
export type ClockMode = "pomodoro" | "countDown" | "countUp";
|