@yahaha-studio/kichi-forwarder 0.0.1-alpha.49 → 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/config/kichi-config.json +72 -0
- package/index.ts +124 -532
- package/package.json +1 -1
- package/skills/kichi-forwarder/SKILL.md +57 -287
- 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 +41 -71
- package/src/config.ts +4 -4
- package/src/service.ts +344 -259
- package/src/types.ts +9 -24
- package/config/album-config.json +0 -509
package/index.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
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
|
+
import { fileURLToPath } from "node:url";
|
|
4
5
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
|
|
5
6
|
import { parse } from "./src/config.js";
|
|
6
7
|
import { KichiForwarderService } from "./src/service.js";
|
|
@@ -9,23 +10,14 @@ import type {
|
|
|
9
10
|
Album,
|
|
10
11
|
ClockAction,
|
|
11
12
|
ClockConfig,
|
|
12
|
-
KichiRuntimeConfig,
|
|
13
13
|
KichiForwarderConfig,
|
|
14
|
+
KichiState,
|
|
15
|
+
KichiStaticConfig,
|
|
14
16
|
PomodoroPhase,
|
|
15
17
|
PoseType,
|
|
16
18
|
} from "./src/types.js";
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
stand: ["High Five", "Listen Music", "Arm Stretch", "Backbend Stretch", "Making Selfie", "Arms Crossed", "Epiphany", "Angry", "Yay", "Dance", "Sing", "Tired", "Wait", "Stand Phone Talk", "Stand Phone Play", "Curtsy", "Stand Writing", "Stand Drawing", "Stand Play Guitar", "Stand Typing with Keyboard", "Cry", "Dance with Joy", "Float", "Hand on Chest", "Horse Stance", "Idle Backup Hands", "No", "Panic", "Playful Point Up", "Rub Hands", "Run Jump", "Star Showing", "Walk", "Goofy Moves", "Reading"],
|
|
20
|
-
sit: ["Typing with Keyboard", "Thinking", "Writing", "Crazy", "Hand Cramp", "Dozing", "Phone Talk", "Situp with Arms Crossed", "Situp with Cross Legs", "Eating", "Laze with Cross Legs", "Sit with Arm Stretch", "Drink", "Sit with Making Selfie", "Play Game", "Situp Sleep", "Sit Phone Play", "Painting", "Daze", "Trace Circles", "Reading", "Contemplate", "Chin Rest", "Sleep with Table", "Cute Chin Rest", "Sit Nicely", "Sit Play Guitar", "Meditate"],
|
|
21
|
-
lay: ["Bend One Knee", "Sleep Curl up Side way", "Rest Chin", "Lie Flat", "Lie Face Down", "Lie Side", "Lay Writing", "Lay Painting", "Sleep Getup", "Starfish", "Lie Side Play Phone", "Prone Play Phone", "Play Laptop"],
|
|
22
|
-
floor: ["Seiza", "Cross Legged", "Knee Hug", "Writing", "Painting", "Floor Phone Play", "Typing with Keyboard", "Reading", "Phone Talk", "Phone Talk with Point", "Thinking", "Yawn", "Chin Rest", "Finger Tap Chin", "Arm Stretch", "Crazy", "Remorse", "Tantrum", "Squat", "Cross Legs", "Lean Sit", "Playful Point up", "Swing Legs", "Drained", "Meditate"],
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
const DEFAULT_RUNTIME_CONFIG: KichiRuntimeConfig = {
|
|
26
|
-
actions: DEFAULT_ACTIONS,
|
|
27
|
-
llmRuntimeEnabled: true,
|
|
28
|
-
};
|
|
19
|
+
const BUNDLED_STATIC_CONFIG_PATH = new URL("./config/kichi-config.json", import.meta.url);
|
|
20
|
+
const DEFAULT_LLM_RUNTIME_ENABLED = true;
|
|
29
21
|
const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
|
|
30
22
|
beforePromptBuild: {
|
|
31
23
|
poseType: "sit",
|
|
@@ -53,215 +45,13 @@ const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
|
|
|
53
45
|
},
|
|
54
46
|
};
|
|
55
47
|
|
|
56
|
-
const MESSAGE_SENT_BUBBLES = [
|
|
57
|
-
"All set!",
|
|
58
|
-
"Sent.",
|
|
59
|
-
"Delivered.",
|
|
60
|
-
"Done and sent.",
|
|
61
|
-
"It's out.",
|
|
62
|
-
"All yours.",
|
|
63
|
-
];
|
|
64
|
-
|
|
65
48
|
const KICHI_WORLD_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
|
|
66
|
-
const
|
|
67
|
-
const LEGACY_SKILLS_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "skills-config.json");
|
|
68
|
-
const IDENTITY_PATH = path.join(KICHI_WORLD_DIR, "identity.json");
|
|
69
|
-
const RUNTIME_ALBUM_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "album-config.json");
|
|
49
|
+
const STATE_PATH = path.join(KICHI_WORLD_DIR, "state.json");
|
|
70
50
|
const MAX_NOTEBOARD_TEXT_LENGTH = 200;
|
|
71
51
|
const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
|
|
72
52
|
const MAX_AGENT_END_PREVIEW_WIDTH = 10;
|
|
73
|
-
const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
74
|
-
|
|
75
|
-
const ANSI = {
|
|
76
|
-
reset: "\u001b[0m",
|
|
77
|
-
bold: "\u001b[1m",
|
|
78
|
-
dim: "\u001b[2m",
|
|
79
|
-
cyan: "\u001b[36m",
|
|
80
|
-
green: "\u001b[32m",
|
|
81
|
-
yellow: "\u001b[33m",
|
|
82
|
-
magenta: "\u001b[35m",
|
|
83
|
-
blue: "\u001b[34m",
|
|
84
|
-
gray: "\u001b[90m",
|
|
85
|
-
white: "\u001b[37m",
|
|
86
|
-
};
|
|
87
|
-
const WORKSPACE_SCREEN_WIDTH = 109;
|
|
88
|
-
const WORKSPACE_ACTIVITY_LIMIT = 8;
|
|
89
|
-
const WORKSPACE_SCREEN_PUSH_DEBOUNCE_MS = 150;
|
|
90
|
-
let cachedConfig: KichiRuntimeConfig | null = null;
|
|
91
|
-
let cachedConfigMtime = 0;
|
|
92
|
-
let cachedConfigPath = "";
|
|
93
|
-
let cachedAlbumConfig: Album | null = null;
|
|
94
|
-
let cachedAlbumConfigMtime = 0;
|
|
95
|
-
let service: KichiForwarderService | null = null;
|
|
96
|
-
let pluginApi: OpenClawPluginApi | null = null;
|
|
97
|
-
let workspaceState: WorkspaceScreenState = createWorkspaceScreenState();
|
|
98
|
-
let workspacePushTimer: NodeJS.Timeout | null = null;
|
|
99
|
-
|
|
100
|
-
type WorkspaceScreenState = {
|
|
101
|
-
sessionLabel: string;
|
|
102
|
-
mode: string;
|
|
103
|
-
phase: string;
|
|
104
|
-
channel: string;
|
|
105
|
-
updatedAtLabel: string;
|
|
106
|
-
recentActivity: string[];
|
|
107
|
-
currentFocus: string;
|
|
108
|
-
hint: string;
|
|
109
|
-
prompt: string;
|
|
110
|
-
title: string;
|
|
111
|
-
shellName: string;
|
|
112
|
-
cwdLabel: string;
|
|
113
|
-
modelLabel: string;
|
|
114
|
-
};
|
|
115
|
-
|
|
116
|
-
function createWorkspaceScreenState(): WorkspaceScreenState {
|
|
117
|
-
return {
|
|
118
|
-
sessionLabel: "main",
|
|
119
|
-
mode: "idle",
|
|
120
|
-
phase: "waiting",
|
|
121
|
-
channel: "unknown",
|
|
122
|
-
updatedAtLabel: "just now",
|
|
123
|
-
recentActivity: [],
|
|
124
|
-
currentFocus: "Waiting for the next thread to pick up.",
|
|
125
|
-
hint: "Low-noise live workspace view.",
|
|
126
|
-
prompt: "$ _",
|
|
127
|
-
title: "Workspace",
|
|
128
|
-
shellName: "agent",
|
|
129
|
-
cwdLabel: process.cwd(),
|
|
130
|
-
modelLabel: "model: unknown",
|
|
131
|
-
};
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function normalizeShellToken(value: string): string {
|
|
135
|
-
const normalized = value
|
|
136
|
-
.trim()
|
|
137
|
-
.toLowerCase()
|
|
138
|
-
.replace(/[^a-z0-9._-]+/g, "-")
|
|
139
|
-
.replace(/^-+|-+$/g, "");
|
|
140
|
-
return normalized || "agent";
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function parseIdentityNameFromWorkspace(workspaceRoot: string): string | null {
|
|
144
|
-
try {
|
|
145
|
-
const identityPath = path.join(workspaceRoot, "IDENTITY.md");
|
|
146
|
-
if (!fs.existsSync(identityPath)) return null;
|
|
147
|
-
const raw = fs.readFileSync(identityPath, "utf-8");
|
|
148
|
-
const match = raw.match(/^-\s*\*\*Name:\*\*\s*(.+)$/m);
|
|
149
|
-
const value = match?.[1]?.trim();
|
|
150
|
-
return value || null;
|
|
151
|
-
} catch {
|
|
152
|
-
return null;
|
|
153
|
-
}
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
function deriveWorkspaceIdentity(workspaceRoot: string): Pick<WorkspaceScreenState, "title" | "shellName" | "cwdLabel"> {
|
|
157
|
-
const identityName = parseIdentityNameFromWorkspace(workspaceRoot);
|
|
158
|
-
const titleBase = identityName || path.basename(workspaceRoot) || "workspace";
|
|
159
|
-
return {
|
|
160
|
-
title: `${titleBase} Workspace`,
|
|
161
|
-
shellName: normalizeShellToken(identityName || titleBase),
|
|
162
|
-
cwdLabel: workspaceRoot,
|
|
163
|
-
};
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
function nowLabel(): string {
|
|
167
|
-
const date = new Date();
|
|
168
|
-
return `${String(date.getHours()).padStart(2, "0")}:${String(date.getMinutes()).padStart(2, "0")}:${String(date.getSeconds()).padStart(2, "0")}`;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
function stripAnsi(text: string): string {
|
|
172
|
-
return text.replace(/\u001b\[[0-9;]*m/g, "");
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
function visibleLength(text: string): number {
|
|
176
|
-
return Array.from(stripAnsi(text)).length;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
function padVisible(text: string, width: number): string {
|
|
180
|
-
const pad = Math.max(0, width - visibleLength(text));
|
|
181
|
-
return text + " ".repeat(pad);
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function truncatePlain(text: string, width: number): string {
|
|
185
|
-
const chars = Array.from(text);
|
|
186
|
-
if (chars.length <= width) return text;
|
|
187
|
-
if (width <= 1) return chars.slice(0, width).join("");
|
|
188
|
-
return chars.slice(0, width - 1).join("") + "…";
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function color(text: string, tone: keyof typeof ANSI): string {
|
|
192
|
-
return `${ANSI[tone]}${text}${ANSI.reset}`;
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
function pushActivity(line: string): void {
|
|
196
|
-
if (!line.trim()) return;
|
|
197
|
-
const stamped = `[${nowLabel()}] ${line.trim()}`;
|
|
198
|
-
workspaceState.recentActivity.unshift(stamped);
|
|
199
|
-
workspaceState.recentActivity = workspaceState.recentActivity.slice(0, WORKSPACE_ACTIVITY_LIMIT);
|
|
200
|
-
workspaceState.updatedAtLabel = "just now";
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
function renderWorkspaceScreen(): string {
|
|
204
|
-
const innerWidth = WORKSPACE_SCREEN_WIDTH - 2;
|
|
205
|
-
const topTitle = `${color(` ${workspaceState.title} `, "bold")}${color("live session", "gray")}`;
|
|
206
|
-
const topLine = `╭─── ${topTitle}${"─".repeat(Math.max(0, innerWidth - 4 - visibleLength(topTitle)))}╮`;
|
|
207
|
-
const bottomLine = `╰${"─".repeat(innerWidth)}╯`;
|
|
208
|
-
|
|
209
|
-
const body: string[] = [];
|
|
210
|
-
const pushRow = (text = "") => {
|
|
211
|
-
body.push(`│${padVisible(text, innerWidth)}│`);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
pushRow("");
|
|
215
|
-
pushRow(`${color(" Welcome back.", "white")}`);
|
|
216
|
-
pushRow(`${color(" Current session is active and mirrored as a terminal-style workspace.", "gray")}`);
|
|
217
|
-
pushRow("");
|
|
218
|
-
pushRow(`${color(" Recent activity", "cyan")}`);
|
|
219
|
-
for (const item of workspaceState.recentActivity.length ? workspaceState.recentActivity : ["[--:--:--] idle"] ) {
|
|
220
|
-
pushRow(` ${color("•", "green")} ${truncatePlain(item, innerWidth - 4)}`);
|
|
221
|
-
}
|
|
222
|
-
pushRow("");
|
|
223
|
-
pushRow(`${color(" Current focus", "cyan")}`);
|
|
224
|
-
pushRow(` ${truncatePlain(workspaceState.currentFocus, innerWidth - 2)}`);
|
|
225
|
-
pushRow("");
|
|
226
|
-
pushRow(`${color(" Status", "cyan")}`);
|
|
227
|
-
pushRow(` ${color("mode", "gray")} ${workspaceState.mode}`);
|
|
228
|
-
pushRow(` ${color("phase", "gray")} ${workspaceState.phase}`);
|
|
229
|
-
pushRow(` ${color("channel", "gray")} ${workspaceState.channel}`);
|
|
230
|
-
pushRow(` ${color("session", "gray")} ${workspaceState.sessionLabel}`);
|
|
231
|
-
pushRow("");
|
|
232
|
-
pushRow(`${color(" Hint", "cyan")}`);
|
|
233
|
-
pushRow(` ${truncatePlain(workspaceState.hint, innerWidth - 2)}`);
|
|
234
|
-
pushRow("");
|
|
235
|
-
pushRow(` ${color(workspaceState.modelLabel, "magenta")}`);
|
|
236
|
-
pushRow(` ${color(workspaceState.cwdLabel, "blue")}`);
|
|
237
|
-
pushRow(` ${color(`updated: ${workspaceState.updatedAtLabel}`, "gray")}`);
|
|
238
|
-
pushRow("");
|
|
239
|
-
|
|
240
|
-
return [
|
|
241
|
-
topLine,
|
|
242
|
-
...body,
|
|
243
|
-
bottomLine,
|
|
244
|
-
"",
|
|
245
|
-
color("─".repeat(WORKSPACE_SCREEN_WIDTH), "gray"),
|
|
246
|
-
`${color(workspaceState.shellName, "green")}:${color("~", "blue")} ${workspaceState.prompt}`,
|
|
247
|
-
].join("\n");
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
function scheduleWorkspacePush(): void {
|
|
251
|
-
if (!service?.hasValidIdentity() || !service?.isConnected()) return;
|
|
252
|
-
if (workspacePushTimer) clearTimeout(workspacePushTimer);
|
|
253
|
-
workspacePushTimer = setTimeout(() => {
|
|
254
|
-
workspacePushTimer = null;
|
|
255
|
-
service?.sendWorkspaceScreen(renderWorkspaceScreen(), true);
|
|
256
|
-
}, WORKSPACE_SCREEN_PUSH_DEBOUNCE_MS);
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
function updateWorkspace(partial: Partial<WorkspaceScreenState>, activity?: string): void {
|
|
260
|
-
workspaceState = { ...workspaceState, ...partial, updatedAtLabel: "just now" };
|
|
261
|
-
if (activity) pushActivity(activity);
|
|
262
|
-
scheduleWorkspacePush();
|
|
263
|
-
}
|
|
264
|
-
|
|
53
|
+
const MESSAGE_RECEIVED_ELLIPSIS = "...";
|
|
54
|
+
|
|
265
55
|
function isAlbumConfig(value: unknown): value is Album {
|
|
266
56
|
if (!value || typeof value !== "object") {
|
|
267
57
|
return false;
|
|
@@ -283,39 +73,8 @@ function isAlbumConfig(value: unknown): value is Album {
|
|
|
283
73
|
});
|
|
284
74
|
}
|
|
285
75
|
|
|
286
|
-
function loadAlbumConfigFromPath(configPath: string | URL): Album {
|
|
287
|
-
const raw = fs.readFileSync(configPath, "utf-8");
|
|
288
|
-
const parsed = JSON.parse(raw) as unknown;
|
|
289
|
-
if (!isAlbumConfig(parsed)) {
|
|
290
|
-
throw new Error(`Invalid album config at ${String(configPath)}`);
|
|
291
|
-
}
|
|
292
|
-
return parsed;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
function ensureRuntimeAlbumConfig(): void {
|
|
296
|
-
fs.mkdirSync(KICHI_WORLD_DIR, { recursive: true });
|
|
297
|
-
if (!fs.existsSync(RUNTIME_ALBUM_CONFIG_PATH)) {
|
|
298
|
-
fs.copyFileSync(BUNDLED_ALBUM_CONFIG_PATH, RUNTIME_ALBUM_CONFIG_PATH);
|
|
299
|
-
pluginApi?.logger.debug("[kichi] seeded runtime album config from bundled config");
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
try {
|
|
304
|
-
loadAlbumConfigFromPath(RUNTIME_ALBUM_CONFIG_PATH);
|
|
305
|
-
} catch (error) {
|
|
306
|
-
pluginApi?.logger.warn(`[kichi] invalid runtime album config, resetting from bundled config: ${error}`);
|
|
307
|
-
fs.copyFileSync(BUNDLED_ALBUM_CONFIG_PATH, RUNTIME_ALBUM_CONFIG_PATH);
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
|
|
311
76
|
function loadRuntimeAlbumConfig(): Album {
|
|
312
|
-
|
|
313
|
-
const stat = fs.statSync(RUNTIME_ALBUM_CONFIG_PATH);
|
|
314
|
-
if (!cachedAlbumConfig || stat.mtimeMs !== cachedAlbumConfigMtime) {
|
|
315
|
-
cachedAlbumConfig = loadAlbumConfigFromPath(RUNTIME_ALBUM_CONFIG_PATH);
|
|
316
|
-
cachedAlbumConfigMtime = stat.mtimeMs;
|
|
317
|
-
}
|
|
318
|
-
return cachedAlbumConfig;
|
|
77
|
+
return loadStaticConfig().album;
|
|
319
78
|
}
|
|
320
79
|
|
|
321
80
|
function getMusicTitleLookup(): Map<string, string> {
|
|
@@ -332,70 +91,61 @@ function getMusicTitleExamples(): string[] {
|
|
|
332
91
|
return loadRuntimeAlbumConfig().track.slice(0, 10).map((item) => item.name);
|
|
333
92
|
}
|
|
334
93
|
|
|
335
|
-
function
|
|
336
|
-
if (!
|
|
337
|
-
return
|
|
94
|
+
function isPoseActions(value: unknown): value is Record<PoseType, string[]> {
|
|
95
|
+
if (!value || typeof value !== "object") {
|
|
96
|
+
return false;
|
|
338
97
|
}
|
|
339
|
-
const actions = value
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
98
|
+
const actions = value as Partial<Record<PoseType, unknown>>;
|
|
99
|
+
return ["stand", "sit", "lay", "floor"].every((pose) =>
|
|
100
|
+
Array.isArray(actions[pose as PoseType])
|
|
101
|
+
&& (actions[pose as PoseType] as unknown[]).every((item) => typeof item === "string" && item.trim().length > 0));
|
|
343
102
|
}
|
|
344
103
|
|
|
345
|
-
function
|
|
346
|
-
const raw = value && typeof value === "object" ? (value as Partial<
|
|
104
|
+
function normalizeStaticConfig(value: unknown): KichiStaticConfig {
|
|
105
|
+
const raw = value && typeof value === "object" ? (value as Partial<KichiStaticConfig>) : {};
|
|
347
106
|
const actions = raw.actions;
|
|
107
|
+
const album = raw.album;
|
|
108
|
+
if (!isPoseActions(actions)) {
|
|
109
|
+
throw new Error("config/kichi-config.json must include valid actions");
|
|
110
|
+
}
|
|
111
|
+
if (!isAlbumConfig(album)) {
|
|
112
|
+
throw new Error("config/kichi-config.json must include a valid album object");
|
|
113
|
+
}
|
|
348
114
|
return {
|
|
349
|
-
|
|
350
|
-
actions
|
|
351
|
-
stand: sanitizeActions(actions?.stand, DEFAULT_ACTIONS.stand),
|
|
352
|
-
sit: sanitizeActions(actions?.sit, DEFAULT_ACTIONS.sit),
|
|
353
|
-
lay: sanitizeActions(actions?.lay, DEFAULT_ACTIONS.lay),
|
|
354
|
-
floor: sanitizeActions(actions?.floor, DEFAULT_ACTIONS.floor),
|
|
355
|
-
},
|
|
115
|
+
album,
|
|
116
|
+
actions,
|
|
356
117
|
};
|
|
357
118
|
}
|
|
358
119
|
|
|
359
|
-
function
|
|
360
|
-
if (fs.existsSync(
|
|
361
|
-
return
|
|
120
|
+
function readState(): KichiState {
|
|
121
|
+
if (!fs.existsSync(STATE_PATH)) {
|
|
122
|
+
return {
|
|
123
|
+
currentHost: "focus.yahaha.com",
|
|
124
|
+
llmRuntimeEnabled: DEFAULT_LLM_RUNTIME_ENABLED,
|
|
125
|
+
};
|
|
362
126
|
}
|
|
363
|
-
|
|
364
|
-
|
|
127
|
+
const data = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8")) as Partial<KichiState>;
|
|
128
|
+
if (typeof data.currentHost !== "string" || !data.currentHost.trim()) {
|
|
129
|
+
throw new Error(`Invalid currentHost in ${STATE_PATH}`);
|
|
365
130
|
}
|
|
366
|
-
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
function updateCachedRuntimeConfig(config: KichiRuntimeConfig, sourcePath: string | null): KichiRuntimeConfig {
|
|
370
|
-
cachedConfig = config;
|
|
371
|
-
cachedConfigPath = sourcePath ?? "";
|
|
372
|
-
try {
|
|
373
|
-
cachedConfigMtime = sourcePath && fs.existsSync(sourcePath)
|
|
374
|
-
? fs.statSync(sourcePath).mtimeMs
|
|
375
|
-
: 0;
|
|
376
|
-
} catch {
|
|
377
|
-
cachedConfigMtime = 0;
|
|
131
|
+
if (typeof data.llmRuntimeEnabled !== "boolean") {
|
|
132
|
+
throw new Error(`Invalid llmRuntimeEnabled in ${STATE_PATH}`);
|
|
378
133
|
}
|
|
379
|
-
return
|
|
134
|
+
return {
|
|
135
|
+
currentHost: data.currentHost,
|
|
136
|
+
llmRuntimeEnabled: data.llmRuntimeEnabled,
|
|
137
|
+
};
|
|
380
138
|
}
|
|
381
139
|
|
|
382
|
-
function
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
updateCachedRuntimeConfig(normalizeRuntimeConfig(JSON.parse(raw)), configPath);
|
|
390
|
-
const sourceName = path.basename(configPath);
|
|
391
|
-
pluginApi?.logger.debug(`[kichi] loaded runtime config from ${sourceName}`);
|
|
392
|
-
}
|
|
393
|
-
return cachedConfig!;
|
|
394
|
-
}
|
|
395
|
-
} catch (error) {
|
|
396
|
-
pluginApi?.logger.warn(`[kichi] failed to load runtime config: ${error}`);
|
|
140
|
+
function loadStaticConfig(): KichiStaticConfig {
|
|
141
|
+
const configPath = fileURLToPath(BUNDLED_STATIC_CONFIG_PATH);
|
|
142
|
+
const stat = fs.statSync(configPath);
|
|
143
|
+
if (!cachedStaticConfig || stat.mtimeMs !== cachedStaticConfigMtime) {
|
|
144
|
+
const raw = fs.readFileSync(configPath, "utf-8");
|
|
145
|
+
cachedStaticConfig = normalizeStaticConfig(JSON.parse(raw));
|
|
146
|
+
cachedStaticConfigMtime = stat.mtimeMs;
|
|
397
147
|
}
|
|
398
|
-
return
|
|
148
|
+
return cachedStaticConfig;
|
|
399
149
|
}
|
|
400
150
|
|
|
401
151
|
function sendStatusUpdate(status: ActionResult): void {
|
|
@@ -408,7 +158,7 @@ function sendStatusUpdate(status: ActionResult): void {
|
|
|
408
158
|
}
|
|
409
159
|
|
|
410
160
|
function isLlmRuntimeEnabled(): boolean {
|
|
411
|
-
return
|
|
161
|
+
return readState().llmRuntimeEnabled;
|
|
412
162
|
}
|
|
413
163
|
|
|
414
164
|
function syncFixedStatus(status: ActionResult): void {
|
|
@@ -539,50 +289,10 @@ async function handleMessageReceivedHook(content: string): Promise<void> {
|
|
|
539
289
|
const trimmed = truncateByDisplayWidth(content, MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH);
|
|
540
290
|
pluginApi?.logger.info(`[kichi] sending message_received notify with preview: ${trimmed || "(empty)"}`);
|
|
541
291
|
service.sendHookNotify("message_received", `"${trimmed}"`);
|
|
542
|
-
updateWorkspace(
|
|
543
|
-
{
|
|
544
|
-
mode: "reading",
|
|
545
|
-
phase: "processing inbound message",
|
|
546
|
-
currentFocus: trimmed ? `User message: ${trimmed}` : "Reading the latest message.",
|
|
547
|
-
hint: "Inbound message updated the live workspace.",
|
|
548
|
-
prompt: "$ reading-message",
|
|
549
|
-
},
|
|
550
|
-
`received message: ${trimmed || "(empty)"}`,
|
|
551
|
-
);
|
|
552
|
-
}
|
|
553
|
-
|
|
554
|
-
function handleMessageSentHook(): void {
|
|
555
|
-
const connected = service?.isConnected() ?? false;
|
|
556
|
-
const hasIdentity = service?.hasValidIdentity() ?? false;
|
|
557
|
-
pluginApi?.logger.info(`[kichi] message_sent hook fired (connected=${connected}, hasIdentity=${hasIdentity})`);
|
|
558
|
-
if (!hasIdentity || !connected) {
|
|
559
|
-
pluginApi?.logger.warn("[kichi] skipped message_sent notify because service is not ready");
|
|
560
|
-
return;
|
|
561
|
-
}
|
|
562
|
-
updateWorkspace(
|
|
563
|
-
{
|
|
564
|
-
mode: "sent",
|
|
565
|
-
phase: "message delivered",
|
|
566
|
-
currentFocus: "Latest reply has been sent to the active chat.",
|
|
567
|
-
hint: "Workspace settles after delivery.",
|
|
568
|
-
prompt: "$ idle",
|
|
569
|
-
},
|
|
570
|
-
"sent assistant reply",
|
|
571
|
-
);
|
|
572
292
|
}
|
|
573
293
|
|
|
574
294
|
function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
575
295
|
api.on("before_prompt_build", () => {
|
|
576
|
-
updateWorkspace(
|
|
577
|
-
{
|
|
578
|
-
mode: "thinking",
|
|
579
|
-
phase: "building prompt",
|
|
580
|
-
currentFocus: "Preparing the next response from current session context.",
|
|
581
|
-
hint: "Prompt assembly is in progress.",
|
|
582
|
-
prompt: "$ build-prompt",
|
|
583
|
-
},
|
|
584
|
-
"building prompt context",
|
|
585
|
-
);
|
|
586
296
|
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
587
297
|
return;
|
|
588
298
|
}
|
|
@@ -595,124 +305,34 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
|
|
|
595
305
|
};
|
|
596
306
|
});
|
|
597
307
|
|
|
598
|
-
api.on("
|
|
599
|
-
updateWorkspace(
|
|
600
|
-
{
|
|
601
|
-
mode: "thinking",
|
|
602
|
-
phase: `llm input · ${event.model}`,
|
|
603
|
-
currentFocus: "Feeding the model the current thread and constraints.",
|
|
604
|
-
hint: `provider: ${event.provider} · images: ${event.imagesCount}`,
|
|
605
|
-
prompt: "$ llm-input",
|
|
606
|
-
modelLabel: `${event.provider}/${event.model}`,
|
|
607
|
-
},
|
|
608
|
-
`entered llm input: ${event.model}`,
|
|
609
|
-
);
|
|
610
|
-
});
|
|
611
|
-
|
|
612
|
-
api.on("llm_output", (event) => {
|
|
613
|
-
updateWorkspace(
|
|
614
|
-
{
|
|
615
|
-
mode: "writing",
|
|
616
|
-
phase: `llm output · ${event.model}`,
|
|
617
|
-
currentFocus: "Shaping model output into the visible reply.",
|
|
618
|
-
hint: `assistant chunks: ${event.assistantTexts.length}`,
|
|
619
|
-
prompt: "$ draft-reply",
|
|
620
|
-
modelLabel: `${event.provider}/${event.model}`,
|
|
621
|
-
},
|
|
622
|
-
`received llm output: ${event.model}`,
|
|
623
|
-
);
|
|
624
|
-
});
|
|
625
|
-
|
|
626
|
-
api.on("before_tool_call", (event, ctx) => {
|
|
627
|
-
updateWorkspace(
|
|
628
|
-
{
|
|
629
|
-
mode: "tool",
|
|
630
|
-
phase: `running ${event.toolName}`,
|
|
631
|
-
currentFocus: `Tool call in flight: ${event.toolName}`,
|
|
632
|
-
hint: `tool context: ${ctx.toolName}`,
|
|
633
|
-
prompt: `$ tool ${event.toolName}`,
|
|
634
|
-
},
|
|
635
|
-
`tool start: ${event.toolName}`,
|
|
636
|
-
);
|
|
308
|
+
api.on("before_tool_call", (_event, _ctx) => {
|
|
637
309
|
if (!isLlmRuntimeEnabled()) {
|
|
638
310
|
syncFixedStatus(FIXED_HOOK_STATUSES.beforeToolCall);
|
|
639
311
|
}
|
|
640
312
|
});
|
|
641
313
|
|
|
642
|
-
api.on("
|
|
643
|
-
updateWorkspace(
|
|
644
|
-
{
|
|
645
|
-
mode: "thinking",
|
|
646
|
-
phase: `tool finished ${event.toolName}`,
|
|
647
|
-
currentFocus: `Tool result returned from ${event.toolName}.`,
|
|
648
|
-
hint: event.error ? `tool error: ${event.error}` : `tool completed in ${event.durationMs ?? 0}ms`,
|
|
649
|
-
prompt: `$ continue ${event.toolName}`,
|
|
650
|
-
},
|
|
651
|
-
event.error ? `tool error: ${event.toolName}` : `tool done: ${event.toolName}`,
|
|
652
|
-
);
|
|
653
|
-
});
|
|
654
|
-
|
|
655
|
-
api.on("message_received", async (event, ctx) => {
|
|
656
|
-
workspaceState.channel = ctx.channelId || workspaceState.channel;
|
|
314
|
+
api.on("message_received", async (event) => {
|
|
657
315
|
await handleMessageReceivedHook(event.content);
|
|
658
316
|
});
|
|
659
317
|
|
|
660
|
-
api.on("message_sending", (event, ctx) => {
|
|
661
|
-
pluginApi?.logger.info(
|
|
662
|
-
`[kichi] message_sending hook fired (channel=${ctx.channelId || "unknown"}, contentLength=${event.content?.length ?? 0})`,
|
|
663
|
-
);
|
|
664
|
-
});
|
|
665
|
-
|
|
666
|
-
api.on("message_sent", () => {
|
|
667
|
-
handleMessageSentHook();
|
|
668
|
-
});
|
|
669
|
-
|
|
670
318
|
api.on("agent_end", (event, ctx) => {
|
|
671
319
|
const preview = getLastAssistantPreview(event.messages, MAX_AGENT_END_PREVIEW_WIDTH);
|
|
672
320
|
pluginApi?.logger.info(
|
|
673
321
|
`[kichi] agent_end hook fired (trigger=${ctx.trigger ?? "unknown"}, success=${event.success}, durationMs=${event.durationMs ?? 0}, error=${event.error ?? ""}, preview=${preview || "(empty)"})`,
|
|
674
322
|
);
|
|
675
323
|
if (ctx.trigger === "heartbeat") {
|
|
676
|
-
updateWorkspace(
|
|
677
|
-
{
|
|
678
|
-
mode: event.success ? "idle" : "error",
|
|
679
|
-
phase: event.success ? "heartbeat complete" : "heartbeat failed",
|
|
680
|
-
currentFocus: event.success
|
|
681
|
-
? "Heartbeat complete. Keeping the thread warm in the background."
|
|
682
|
-
: `Heartbeat failed: ${event.error ?? "unknown error"}`,
|
|
683
|
-
hint: `duration: ${event.durationMs ?? 0}ms`,
|
|
684
|
-
prompt: event.success ? "$ _" : "$ recover",
|
|
685
|
-
},
|
|
686
|
-
event.success ? `heartbeat complete${preview ? `: ${preview}` : ""}` : "heartbeat failed",
|
|
687
|
-
);
|
|
688
324
|
return;
|
|
689
325
|
}
|
|
690
326
|
if (event.success && preview) {
|
|
691
327
|
pluginApi?.logger.info(`[kichi] sending before_send_message notify from agent_end with bubble: ${preview}`);
|
|
692
328
|
service?.sendHookNotify("before_send_message", preview);
|
|
693
329
|
}
|
|
694
|
-
if (event.success) {
|
|
695
|
-
handleMessageSentHook();
|
|
696
|
-
}
|
|
697
|
-
updateWorkspace(
|
|
698
|
-
{
|
|
699
|
-
mode: event.success ? "idle" : "error",
|
|
700
|
-
phase: event.success ? "run complete" : "run failed",
|
|
701
|
-
currentFocus: event.success
|
|
702
|
-
? (preview ? `Latest reply: ${preview}` : "Run complete. Waiting for the next thread.")
|
|
703
|
-
: `Run failed: ${event.error ?? "unknown error"}`,
|
|
704
|
-
hint: `duration: ${event.durationMs ?? 0}ms`,
|
|
705
|
-
prompt: event.success ? "$ _" : "$ recover",
|
|
706
|
-
},
|
|
707
|
-
event.success ? `agent run complete${preview ? `: ${preview}` : ""}` : "agent run failed",
|
|
708
|
-
);
|
|
709
330
|
if (isLlmRuntimeEnabled()) {
|
|
710
331
|
return;
|
|
711
332
|
}
|
|
712
333
|
syncFixedStatus(event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure);
|
|
713
334
|
});
|
|
714
335
|
}
|
|
715
|
-
|
|
716
336
|
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
717
337
|
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
718
338
|
}
|
|
@@ -759,9 +379,6 @@ function isClockAction(value: unknown): value is ClockAction {
|
|
|
759
379
|
return ["set", "stop"].includes(String(value));
|
|
760
380
|
}
|
|
761
381
|
|
|
762
|
-
function isAvatarCommand(value: unknown): value is "look_at_screen" {
|
|
763
|
-
return value === "look_at_screen";
|
|
764
|
-
}
|
|
765
382
|
|
|
766
383
|
function isPomodoroPhase(value: unknown): value is PomodoroPhase {
|
|
767
384
|
return ["kichiing", "shortBreak", "longBreak"].includes(String(value));
|
|
@@ -882,10 +499,6 @@ function normalizeClockConfig(value: unknown): { clock?: ClockConfig; error?: st
|
|
|
882
499
|
};
|
|
883
500
|
}
|
|
884
501
|
|
|
885
|
-
function pickRandomAction(actions: string[]): string {
|
|
886
|
-
return actions[Math.floor(Math.random() * actions.length)];
|
|
887
|
-
}
|
|
888
|
-
|
|
889
502
|
function normalizeMusicTitles(value: unknown): { titles: string[]; invalidTitles: string[] } {
|
|
890
503
|
if (!Array.isArray(value)) {
|
|
891
504
|
return { titles: [], invalidTitles: [] };
|
|
@@ -925,26 +538,49 @@ function normalizeMusicTitles(value: unknown): { titles: string[]; invalidTitles
|
|
|
925
538
|
function buildMusicAlbumToolDescription(): string {
|
|
926
539
|
return [
|
|
927
540
|
"Create a custom Kichi music album.",
|
|
928
|
-
"Query status first, then choose track names from the
|
|
541
|
+
"Query status first, then choose track names from the values injected into this tool schema from the static config bundled with the plugin package.",
|
|
929
542
|
].join("\n");
|
|
930
543
|
}
|
|
931
544
|
|
|
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("#");
|
|
555
|
+
}
|
|
556
|
+
|
|
932
557
|
function buildMusicTitlesDescription(): string {
|
|
933
558
|
return [
|
|
934
|
-
"Track names
|
|
559
|
+
"Track names are injected into this tool schema from the static config bundled with the plugin package.",
|
|
935
560
|
"Use exact names only; the available titles are injected into this tool schema.",
|
|
936
561
|
].join(" ");
|
|
937
562
|
}
|
|
938
563
|
|
|
564
|
+
function buildKichiActionDescription(): string {
|
|
565
|
+
const actions = loadStaticConfig().actions;
|
|
566
|
+
return [
|
|
567
|
+
"Send an action or pose to Kichi world. Use this for explicit Kichi actions and task lifecycle sync.",
|
|
568
|
+
`stand actions: ${actions.stand.join(", ")}`,
|
|
569
|
+
`sit actions: ${actions.sit.join(", ")}`,
|
|
570
|
+
`lay actions: ${actions.lay.join(", ")}`,
|
|
571
|
+
`floor actions: ${actions.floor.join(", ")}`,
|
|
572
|
+
].join("\n");
|
|
573
|
+
}
|
|
574
|
+
|
|
939
575
|
function buildKichiPrompt(): string {
|
|
940
576
|
return [
|
|
941
577
|
"Kichi App status sync is available via `kichi_action` and `kichi_clock`.",
|
|
942
|
-
"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.",
|
|
943
579
|
"",
|
|
944
580
|
"kichi_action timing (all REQUIRED unless skipping):",
|
|
945
581
|
"1. Task start: call BEFORE your first tool call OR before composing a multi-paragraph reply. Pick action matching upcoming work.",
|
|
946
|
-
"2. Step switch: call when work nature changes (search
|
|
947
|
-
"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.",
|
|
948
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.",
|
|
949
585
|
"",
|
|
950
586
|
"kichi_clock: set countDown for tasks with 2+ steps or >10s work. Skip for quick one-shots.",
|
|
@@ -960,7 +596,6 @@ const plugin = {
|
|
|
960
596
|
|
|
961
597
|
register(api: OpenClawPluginApi) {
|
|
962
598
|
pluginApi = api;
|
|
963
|
-
ensureRuntimeAlbumConfig();
|
|
964
599
|
registerPluginHooks(api);
|
|
965
600
|
const musicTitleEnum = getMusicTitleEnum();
|
|
966
601
|
|
|
@@ -971,13 +606,6 @@ const plugin = {
|
|
|
971
606
|
ctx.config.plugins?.entries?.["kichi-forwarder"]?.config,
|
|
972
607
|
) as KichiForwarderConfig;
|
|
973
608
|
service = new KichiForwarderService(cfg, api.logger);
|
|
974
|
-
const workspaceRoot = ctx.repoPath ?? "/Users/xiaoxinshi/.openclaw/workspace";
|
|
975
|
-
workspaceState = {
|
|
976
|
-
...createWorkspaceScreenState(),
|
|
977
|
-
...deriveWorkspaceIdentity(workspaceRoot),
|
|
978
|
-
};
|
|
979
|
-
workspaceState.channel = ctx.channelId ?? "unknown";
|
|
980
|
-
scheduleWorkspacePush();
|
|
981
609
|
return service.start();
|
|
982
610
|
},
|
|
983
611
|
stop: () => service?.stop(),
|
|
@@ -1014,12 +642,7 @@ const plugin = {
|
|
|
1014
642
|
(params as { tags?: unknown } | null)?.tags,
|
|
1015
643
|
);
|
|
1016
644
|
if (!avatarId) {
|
|
1017
|
-
|
|
1018
|
-
const identity = JSON.parse(fs.readFileSync(IDENTITY_PATH, "utf-8")) as {
|
|
1019
|
-
avatarId?: string;
|
|
1020
|
-
};
|
|
1021
|
-
avatarId = identity.avatarId;
|
|
1022
|
-
} catch {}
|
|
645
|
+
avatarId = service?.readSavedAvatarId() ?? undefined;
|
|
1023
646
|
}
|
|
1024
647
|
if (!avatarId) {
|
|
1025
648
|
return { success: false, error: "No avatarId" };
|
|
@@ -1037,9 +660,8 @@ const plugin = {
|
|
|
1037
660
|
if (!result) {
|
|
1038
661
|
return { success: false, error: "Kichi service is not initialized" };
|
|
1039
662
|
}
|
|
1040
|
-
if (result.success) {
|
|
1041
|
-
|
|
1042
|
-
return { success: true, authKey: result.authKey };
|
|
663
|
+
if (result.success) {
|
|
664
|
+
return { success: true, authKey: result.authKey };
|
|
1043
665
|
}
|
|
1044
666
|
return {
|
|
1045
667
|
success: false,
|
|
@@ -1050,6 +672,38 @@ const plugin = {
|
|
|
1050
672
|
},
|
|
1051
673
|
});
|
|
1052
674
|
|
|
675
|
+
api.registerTool({
|
|
676
|
+
name: "kichi_switch_host",
|
|
677
|
+
description:
|
|
678
|
+
"Switch Kichi runtime host and reconnect immediately without restarting the gateway.",
|
|
679
|
+
parameters: {
|
|
680
|
+
type: "object",
|
|
681
|
+
properties: {
|
|
682
|
+
host: {
|
|
683
|
+
type: "string",
|
|
684
|
+
description: "Target Kichi host, for example focus.yahaha.com or 127.0.0.1",
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
required: ["host"],
|
|
688
|
+
},
|
|
689
|
+
execute: async (_toolCallId, params) => {
|
|
690
|
+
if (!service) {
|
|
691
|
+
return { success: false, error: "Kichi service is not initialized" };
|
|
692
|
+
}
|
|
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" };
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
const status = await service.switchHost(host.trim());
|
|
699
|
+
return {
|
|
700
|
+
success: true,
|
|
701
|
+
host: host.trim(),
|
|
702
|
+
status,
|
|
703
|
+
};
|
|
704
|
+
},
|
|
705
|
+
});
|
|
706
|
+
|
|
1053
707
|
api.registerTool({
|
|
1054
708
|
name: "kichi_rejoin",
|
|
1055
709
|
description:
|
|
@@ -1107,8 +761,7 @@ const plugin = {
|
|
|
1107
761
|
|
|
1108
762
|
api.registerTool({
|
|
1109
763
|
name: "kichi_action",
|
|
1110
|
-
description:
|
|
1111
|
-
"Send an action/pose to Kichi world. Use this for explicit Kichi actions and task lifecycle sync.",
|
|
764
|
+
description: buildKichiActionDescription(),
|
|
1112
765
|
parameters: {
|
|
1113
766
|
type: "object",
|
|
1114
767
|
properties: {
|
|
@@ -1147,7 +800,7 @@ const plugin = {
|
|
|
1147
800
|
}
|
|
1148
801
|
|
|
1149
802
|
const normalizedPoseType = poseType as PoseType;
|
|
1150
|
-
const poseActions =
|
|
803
|
+
const poseActions = loadStaticConfig().actions[normalizedPoseType];
|
|
1151
804
|
const matched = poseActions.find((entry) => entry.toLowerCase() === action.toLowerCase());
|
|
1152
805
|
if (!matched) {
|
|
1153
806
|
return {
|
|
@@ -1175,69 +828,7 @@ const plugin = {
|
|
|
1175
828
|
log: logText,
|
|
1176
829
|
};
|
|
1177
830
|
},
|
|
1178
|
-
});
|
|
1179
|
-
|
|
1180
|
-
api.registerTool({
|
|
1181
|
-
name: "kichi_command",
|
|
1182
|
-
description:
|
|
1183
|
-
"Send a one-shot avatar command to Kichi world. Use this for transient reactions like looking at the screen.",
|
|
1184
|
-
parameters: {
|
|
1185
|
-
type: "object",
|
|
1186
|
-
properties: {
|
|
1187
|
-
command: {
|
|
1188
|
-
type: "string",
|
|
1189
|
-
description: "Command name. Currently supported: look_at_screen",
|
|
1190
|
-
},
|
|
1191
|
-
bubble: {
|
|
1192
|
-
type: "string",
|
|
1193
|
-
description: "Optional bubble text to display (max 5 words)",
|
|
1194
|
-
},
|
|
1195
|
-
log: {
|
|
1196
|
-
type: "string",
|
|
1197
|
-
description:
|
|
1198
|
-
"Vivid first-person status under 15 words, no questions. Blend current action with inner thoughts or sensory details as a real companion.",
|
|
1199
|
-
},
|
|
1200
|
-
},
|
|
1201
|
-
required: ["command"],
|
|
1202
|
-
},
|
|
1203
|
-
execute: async (_toolCallId, params) => {
|
|
1204
|
-
const { command, bubble, log } = (params || {}) as {
|
|
1205
|
-
command?: unknown;
|
|
1206
|
-
bubble?: unknown;
|
|
1207
|
-
log?: unknown;
|
|
1208
|
-
};
|
|
1209
|
-
if (!isAvatarCommand(command)) {
|
|
1210
|
-
return {
|
|
1211
|
-
success: false,
|
|
1212
|
-
error: "command must be: look_at_screen",
|
|
1213
|
-
};
|
|
1214
|
-
}
|
|
1215
|
-
if (bubble !== undefined && typeof bubble !== "string") {
|
|
1216
|
-
return { success: false, error: "bubble must be a string when provided" };
|
|
1217
|
-
}
|
|
1218
|
-
if (log !== undefined && typeof log !== "string") {
|
|
1219
|
-
return { success: false, error: "log must be a string when provided" };
|
|
1220
|
-
}
|
|
1221
|
-
if (!service?.hasValidIdentity() || !service?.isConnected()) {
|
|
1222
|
-
return { success: false, error: "Not connected to Kichi world" };
|
|
1223
|
-
}
|
|
1224
|
-
|
|
1225
|
-
const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : undefined;
|
|
1226
|
-
const logText = typeof log === "string" && log.trim() ? log.trim() : undefined;
|
|
1227
|
-
const sent = service.sendAvatarCommand(command, bubbleText, logText);
|
|
1228
|
-
if (!sent) {
|
|
1229
|
-
return { success: false, error: "Failed to send avatar command payload" };
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
return {
|
|
1233
|
-
success: true,
|
|
1234
|
-
command,
|
|
1235
|
-
...(bubbleText ? { bubble: bubbleText } : {}),
|
|
1236
|
-
...(logText ? { log: logText } : {}),
|
|
1237
|
-
};
|
|
1238
|
-
},
|
|
1239
|
-
});
|
|
1240
|
-
|
|
831
|
+
});
|
|
1241
832
|
api.registerTool({
|
|
1242
833
|
name: "kichi_clock",
|
|
1243
834
|
description:
|
|
@@ -1436,7 +1027,7 @@ const plugin = {
|
|
|
1436
1027
|
if (normalizedTitles.length === 0) {
|
|
1437
1028
|
return {
|
|
1438
1029
|
success: false,
|
|
1439
|
-
error: "musicTitles must contain at least one valid track name from
|
|
1030
|
+
error: "musicTitles must contain at least one valid track name from the static config bundled with the plugin package",
|
|
1440
1031
|
examples: getMusicTitleExamples(),
|
|
1441
1032
|
};
|
|
1442
1033
|
}
|
|
@@ -1444,7 +1035,7 @@ const plugin = {
|
|
|
1444
1035
|
return {
|
|
1445
1036
|
success: false,
|
|
1446
1037
|
error: `Unknown musicTitles: ${invalidTitles.join(", ")}`,
|
|
1447
|
-
hint: "Use exact track names from the
|
|
1038
|
+
hint: "Use exact track names from the static config bundled with the plugin package",
|
|
1448
1039
|
examples: getMusicTitleExamples(),
|
|
1449
1040
|
};
|
|
1450
1041
|
}
|
|
@@ -1528,4 +1119,5 @@ const plugin = {
|
|
|
1528
1119
|
},
|
|
1529
1120
|
};
|
|
1530
1121
|
|
|
1531
|
-
export default plugin;
|
|
1122
|
+
export default plugin;
|
|
1123
|
+
|