@yahaha-studio/kichi-forwarder 0.0.1-alpha.49 → 0.0.1-alpha.50

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.
@@ -0,0 +1,72 @@
1
+ {
2
+ "actions": {
3
+ "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"],
4
+ "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"],
5
+ "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"],
6
+ "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"]
7
+ },
8
+ "album": {
9
+ "albumCount": 10,
10
+ "trackCount": 58,
11
+ "track": [
12
+ { "album": "Dusty Bookshelves", "name": "A moment of calm", "tags": ["Gentle", "Relaxing", "Easy listening"] },
13
+ { "album": "Dusty Bookshelves", "name": "Closed Love", "tags": ["Dark ", "Magnificent"] },
14
+ { "album": "Dusty Bookshelves", "name": "In That Mood", "tags": ["Lounge Bar Cafe", "Relaxing"] },
15
+ { "album": "Dusty Bookshelves", "name": "Let It Happen", "tags": ["Lounge Restaurant Cafe", "Light"] },
16
+ { "album": "Dusty Bookshelves", "name": "Memories forever", "tags": ["Moving", "Gentle", "Warm", "Sad ballad"] },
17
+ { "album": "Dusty Bookshelves", "name": "Outer Space", "tags": ["Space ", "Magnificent"] },
18
+ { "album": "Dusty Bookshelves", "name": "Spiritual world", "tags": ["Inorganic", "Unrealistic", "Simple"] },
19
+ { "album": "Late Night Coder", "name": "Calm Time", "tags": ["Calm ", "Gentle"] },
20
+ { "album": "Late Night Coder", "name": "Counting the Stars", "tags": ["Gentle", "Calm", "Sleep", "Night", "Easy listening"] },
21
+ { "album": "Late Night Coder", "name": "Deserted landscape", "tags": ["Inorganic", "5 beats Polyrhythm"] },
22
+ { "album": "Late Night Coder", "name": "Dog Days", "tags": ["Slightly dark", "Drowsy"] },
23
+ { "album": "Late Night Coder", "name": "It Was an Unpleasant Incident", "tags": ["Gentle", "Slightly sad"] },
24
+ { "album": "Late Night Coder", "name": "The Dark Eternal Night", "tags": ["Slightly dark", "Eerie"] },
25
+ { "album": "Midnight Cyber-Transit", "name": "Afterimage Metaphor", "tags": ["Sad ", "Lonely", "Transparent"] },
26
+ { "album": "Midnight Cyber-Transit", "name": "I keep looking at the floor", "tags": ["Somewhat dark", "Empty"] },
27
+ { "album": "Midnight Cyber-Transit", "name": "Interstellar matter", "tags": ["Space ", "Floating feeling"] },
28
+ { "album": "Midnight Cyber-Transit", "name": "Special To Me", "tags": ["Bebop jazz", "Light", "Radio background music"] },
29
+ { "album": "Midnight Cyber-Transit", "name": "Specification", "tags": ["Slightly happy", "General"] },
30
+ { "album": "Morning Dew & Drip Coffee", "name": "Daytime activities", "tags": ["General", "Languid", "Daytime"] },
31
+ { "album": "Morning Dew & Drip Coffee", "name": "Enjoy the rest of your day", "tags": ["Comical", "Cheerful", "Fun", "Simple"] },
32
+ { "album": "Morning Dew & Drip Coffee", "name": "Entertainment Committee", "tags": ["Slightly bright", "Relaxing"] },
33
+ { "album": "Morning Dew & Drip Coffee", "name": "Heavy Rain", "tags": ["Inorganic", "Mysterious"] },
34
+ { "album": "Morning Dew & Drip Coffee", "name": "Lovely Day", "tags": ["Bright", "Gentle", "Calm"] },
35
+ { "album": "Morning Dew & Drip Coffee", "name": "Relax in the shade", "tags": ["Bright", "Kind", "Warm", "Fun", "Relaxing"] },
36
+ { "album": "Morning Dew & Drip Coffee", "name": "Spring is a nap", "tags": ["Relaxing", "Gentle", "Cheerful", "Easy listening"] },
37
+ { "album": "Morning Dew & Drip Coffee", "name": "Warm tea time", "tags": ["Slightly bright", "Gentle", "Ennui"] },
38
+ { "album": "Nostalgic Frequencies", "name": "Full of Energy", "tags": ["Slightly bright", "Uplifting", "Medium tempo"] },
39
+ { "album": "Nostalgic Frequencies", "name": "Someday in the Rain", "tags": ["Slightly bright", "Moist"] },
40
+ { "album": "Nostalgic Frequencies", "name": "Still in Love", "tags": ["Slightly sad"] },
41
+ { "album": "Nostalgic Frequencies", "name": "Trace", "tags": ["Melancholic", "Sentimental", "Nostalgic"] },
42
+ { "album": "Nostalgic Frequencies", "name": "Water Town", "tags": ["Slightly bright", "Gentle", "Simple"] },
43
+ { "album": "Overgrown Ruins", "name": "Blank and Silence", "tags": ["Light", "General purpose"] },
44
+ { "album": "Overgrown Ruins", "name": "Darkness of the New Moon", "tags": ["Dark", "Horror"] },
45
+ { "album": "Overgrown Ruins", "name": "Flowers that Bloom in Sadness", "tags": ["Slightly dark ", "Sad"] },
46
+ { "album": "Overgrown Ruins", "name": "The Price of Mistakes", "tags": ["Sad", "Painful", "Serious", "Flashback"] },
47
+ { "album": "Raindrops on the Windowpane", "name": "A little love there", "tags": ["Gentle ", "Slumber"] },
48
+ { "album": "Raindrops on the Windowpane", "name": "Beside You", "tags": ["Moving", "Gentle", "Warm", "Sad ballad"] },
49
+ { "album": "Raindrops on the Windowpane", "name": "Blue Star", "tags": ["Calm", "Night"] },
50
+ { "album": "Raindrops on the Windowpane", "name": "I want to get to know you", "tags": ["Touching", "Sad", "Kind"] },
51
+ { "album": "Raindrops on the Windowpane", "name": "Passing each other", "tags": ["Slightly dark", "Calm", "Weak"] },
52
+ { "album": "Raindrops on the Windowpane", "name": "Pieces of a Dream", "tags": ["Lounge Bar Relaxation"] },
53
+ { "album": "Raindrops on the Windowpane", "name": "Puzzle", "tags": ["Slightly dark", "Calm"] },
54
+ { "album": "Raindrops on the Windowpane", "name": "The meaning of tears", "tags": ["Touching ", "Mellow"] },
55
+ { "album": "Sunday Laundry", "name": "Abandoned cat in the Rain", "tags": ["Little dark", "Lonely"] },
56
+ { "album": "Sunday Laundry", "name": "Fairy Forest", "tags": ["Bright ", "Fantasy ", "Lively"] },
57
+ { "album": "Sunday Laundry", "name": "In the sunlight filtering through the trees", "tags": ["Bright", "Relaxed", "Peaceful"] },
58
+ { "album": "Sunday Laundry", "name": "Perfect Weather for a Walk", "tags": ["Cute", "Bright", "Simple", "Fun", "Relaxing"] },
59
+ { "album": "Sunday Laundry", "name": "Rural Prairie", "tags": ["Bright", "Pastoral", "Gentle"] },
60
+ { "album": "Sunday Laundry", "name": "Spring Classroom", "tags": ["Bright", "Gentle"] },
61
+ { "album": "Sunset at the Pier", "name": "Deep Sea Passage", "tags": ["Slightly bright", "Inorganic"] },
62
+ { "album": "Sunset at the Pier", "name": "Memory", "tags": ["Slightly dark", "Magnificent"] },
63
+ { "album": "Sunset at the Pier", "name": "Smiles and Tears", "tags": ["Slightly bright", "Sad"] },
64
+ { "album": "Sunset at the Pier", "name": "Surrounded by Silence", "tags": ["Gentle", "Calm", "Stylish", "Easy listening"] },
65
+ { "album": "Zenith Garden", "name": "Cold Sweat", "tags": ["Comical", "Failure", "Simple"] },
66
+ { "album": "Zenith Garden", "name": "Hometown Sunset", "tags": ["Slightly dark", "Gentle", "Simple"] },
67
+ { "album": "Zenith Garden", "name": "Nostalgia", "tags": ["Moving", "Sad", "Melancholy", "Lonely"] },
68
+ { "album": "Zenith Garden", "name": "Reverie", "tags": ["Slightly dark", "Transparent"] },
69
+ { "album": "Zenith Garden", "name": "Water Cave", "tags": ["Slightly dark", "Inorganic", "Transparent"] }
70
+ ]
71
+ }
72
+ }
package/index.ts CHANGED
@@ -1,6 +1,7 @@
1
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,15 @@ import type {
9
10
  Album,
10
11
  ClockAction,
11
12
  ClockConfig,
12
- KichiRuntimeConfig,
13
+ KichiEnv,
13
14
  KichiForwarderConfig,
15
+ KichiState,
16
+ KichiStaticConfig,
14
17
  PomodoroPhase,
15
18
  PoseType,
16
19
  } from "./src/types.js";
17
-
18
- const DEFAULT_ACTIONS: KichiRuntimeConfig["actions"] = {
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
- };
20
+ const BUNDLED_STATIC_CONFIG_PATH = new URL("./config/kichi-config.json", import.meta.url);
21
+ const DEFAULT_LLM_RUNTIME_ENABLED = true;
29
22
  const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
30
23
  beforePromptBuild: {
31
24
  poseType: "sit",
@@ -53,25 +46,12 @@ const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
53
46
  },
54
47
  };
55
48
 
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
49
  const KICHI_WORLD_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
66
- const RUNTIME_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "kichi-runtime-config.json");
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");
50
+ const STATE_PATH = path.join(KICHI_WORLD_DIR, "state.json");
70
51
  const MAX_NOTEBOARD_TEXT_LENGTH = 200;
71
52
  const MAX_MESSAGE_RECEIVED_PREVIEW_WIDTH = 20;
72
53
  const MAX_AGENT_END_PREVIEW_WIDTH = 10;
73
54
  const MESSAGE_RECEIVED_ELLIPSIS = "...";
74
- const BUNDLED_ALBUM_CONFIG_PATH = new URL("./config/album-config.json", import.meta.url);
75
55
  const ANSI = {
76
56
  reset: "\u001b[0m",
77
57
  bold: "\u001b[1m",
@@ -87,11 +67,8 @@ const ANSI = {
87
67
  const WORKSPACE_SCREEN_WIDTH = 109;
88
68
  const WORKSPACE_ACTIVITY_LIMIT = 8;
89
69
  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;
70
+ let cachedStaticConfig: KichiStaticConfig | null = null;
71
+ let cachedStaticConfigMtime = 0;
95
72
  let service: KichiForwarderService | null = null;
96
73
  let pluginApi: OpenClawPluginApi | null = null;
97
74
  let workspaceState: WorkspaceScreenState = createWorkspaceScreenState();
@@ -283,39 +260,8 @@ function isAlbumConfig(value: unknown): value is Album {
283
260
  });
284
261
  }
285
262
 
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
263
  function loadRuntimeAlbumConfig(): Album {
312
- ensureRuntimeAlbumConfig();
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;
264
+ return loadStaticConfig().album;
319
265
  }
320
266
 
321
267
  function getMusicTitleLookup(): Map<string, string> {
@@ -332,70 +278,61 @@ function getMusicTitleExamples(): string[] {
332
278
  return loadRuntimeAlbumConfig().track.slice(0, 10).map((item) => item.name);
333
279
  }
334
280
 
335
- function sanitizeActions(value: unknown, fallback: string[]): string[] {
336
- if (!Array.isArray(value)) {
337
- return fallback;
281
+ function isPoseActions(value: unknown): value is Record<PoseType, string[]> {
282
+ if (!value || typeof value !== "object") {
283
+ return false;
338
284
  }
339
- const actions = value.filter(
340
- (item): item is string => typeof item === "string" && item.trim().length > 0,
341
- );
342
- return actions.length > 0 ? actions : fallback;
285
+ const actions = value as Partial<Record<PoseType, unknown>>;
286
+ return ["stand", "sit", "lay", "floor"].every((pose) =>
287
+ Array.isArray(actions[pose as PoseType])
288
+ && (actions[pose as PoseType] as unknown[]).every((item) => typeof item === "string" && item.trim().length > 0));
343
289
  }
344
290
 
345
- function normalizeRuntimeConfig(value: unknown): KichiRuntimeConfig {
346
- const raw = value && typeof value === "object" ? (value as Partial<KichiRuntimeConfig>) : {};
291
+ function normalizeStaticConfig(value: unknown): KichiStaticConfig {
292
+ const raw = value && typeof value === "object" ? (value as Partial<KichiStaticConfig>) : {};
347
293
  const actions = raw.actions;
294
+ const album = raw.album;
295
+ if (!isPoseActions(actions)) {
296
+ throw new Error("config/kichi-config.json must include valid actions");
297
+ }
298
+ if (!isAlbumConfig(album)) {
299
+ throw new Error("config/kichi-config.json must include a valid album object");
300
+ }
348
301
  return {
349
- llmRuntimeEnabled: typeof raw.llmRuntimeEnabled === "boolean" ? raw.llmRuntimeEnabled : true,
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
- },
302
+ album,
303
+ actions,
356
304
  };
357
305
  }
358
306
 
359
- function resolveRuntimeConfigPath(): string | null {
360
- if (fs.existsSync(RUNTIME_CONFIG_PATH)) {
361
- return RUNTIME_CONFIG_PATH;
307
+ function readState(): KichiState {
308
+ if (!fs.existsSync(STATE_PATH)) {
309
+ return {
310
+ currentEnv: "prod",
311
+ llmRuntimeEnabled: DEFAULT_LLM_RUNTIME_ENABLED,
312
+ };
362
313
  }
363
- if (fs.existsSync(LEGACY_SKILLS_CONFIG_PATH)) {
364
- return LEGACY_SKILLS_CONFIG_PATH;
314
+ const data = JSON.parse(fs.readFileSync(STATE_PATH, "utf-8")) as Partial<KichiState>;
315
+ if (data.currentEnv !== "local" && data.currentEnv !== "dev" && data.currentEnv !== "prod") {
316
+ throw new Error(`Invalid currentEnv in ${STATE_PATH}`);
365
317
  }
366
- return null;
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;
318
+ if (typeof data.llmRuntimeEnabled !== "boolean") {
319
+ throw new Error(`Invalid llmRuntimeEnabled in ${STATE_PATH}`);
378
320
  }
379
- return config;
321
+ return {
322
+ currentEnv: data.currentEnv,
323
+ llmRuntimeEnabled: data.llmRuntimeEnabled,
324
+ };
380
325
  }
381
326
 
382
- function loadRuntimeConfig(): KichiRuntimeConfig {
383
- try {
384
- const configPath = resolveRuntimeConfigPath();
385
- if (configPath) {
386
- const stat = fs.statSync(configPath);
387
- if (configPath !== cachedConfigPath || stat.mtimeMs !== cachedConfigMtime || !cachedConfig) {
388
- const raw = fs.readFileSync(configPath, "utf-8");
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}`);
327
+ function loadStaticConfig(): KichiStaticConfig {
328
+ const configPath = fileURLToPath(BUNDLED_STATIC_CONFIG_PATH);
329
+ const stat = fs.statSync(configPath);
330
+ if (!cachedStaticConfig || stat.mtimeMs !== cachedStaticConfigMtime) {
331
+ const raw = fs.readFileSync(configPath, "utf-8");
332
+ cachedStaticConfig = normalizeStaticConfig(JSON.parse(raw));
333
+ cachedStaticConfigMtime = stat.mtimeMs;
397
334
  }
398
- return updateCachedRuntimeConfig(DEFAULT_RUNTIME_CONFIG, null);
335
+ return cachedStaticConfig;
399
336
  }
400
337
 
401
338
  function sendStatusUpdate(status: ActionResult): void {
@@ -408,7 +345,7 @@ function sendStatusUpdate(status: ActionResult): void {
408
345
  }
409
346
 
410
347
  function isLlmRuntimeEnabled(): boolean {
411
- return loadRuntimeConfig().llmRuntimeEnabled;
348
+ return readState().llmRuntimeEnabled;
412
349
  }
413
350
 
414
351
  function syncFixedStatus(status: ActionResult): void {
@@ -882,10 +819,6 @@ function normalizeClockConfig(value: unknown): { clock?: ClockConfig; error?: st
882
819
  };
883
820
  }
884
821
 
885
- function pickRandomAction(actions: string[]): string {
886
- return actions[Math.floor(Math.random() * actions.length)];
887
- }
888
-
889
822
  function normalizeMusicTitles(value: unknown): { titles: string[]; invalidTitles: string[] } {
890
823
  if (!Array.isArray(value)) {
891
824
  return { titles: [], invalidTitles: [] };
@@ -925,17 +858,32 @@ function normalizeMusicTitles(value: unknown): { titles: string[]; invalidTitles
925
858
  function buildMusicAlbumToolDescription(): string {
926
859
  return [
927
860
  "Create a custom Kichi music album.",
928
- "Query status first, then choose track names from the runtime album config: Linux/macOS `~/.openclaw/kichi-world/album-config.json`; Windows `%USERPROFILE%\\.openclaw\\kichi-world\\album-config.json`.",
861
+ "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
862
  ].join("\n");
930
863
  }
931
864
 
865
+ function isKichiEnv(value: unknown): value is KichiEnv {
866
+ return value === "local" || value === "dev" || value === "prod";
867
+ }
868
+
932
869
  function buildMusicTitlesDescription(): string {
933
870
  return [
934
- "Track names chosen from the runtime album config: Linux/macOS `~/.openclaw/kichi-world/album-config.json`; Windows `%USERPROFILE%\\.openclaw\\kichi-world\\album-config.json`.",
871
+ "Track names are injected into this tool schema from the static config bundled with the plugin package.",
935
872
  "Use exact names only; the available titles are injected into this tool schema.",
936
873
  ].join(" ");
937
874
  }
938
875
 
876
+ function buildKichiActionDescription(): string {
877
+ const actions = loadStaticConfig().actions;
878
+ return [
879
+ "Send an action or pose to Kichi world. Use this for explicit Kichi actions and task lifecycle sync.",
880
+ `stand actions: ${actions.stand.join(", ")}`,
881
+ `sit actions: ${actions.sit.join(", ")}`,
882
+ `lay actions: ${actions.lay.join(", ")}`,
883
+ `floor actions: ${actions.floor.join(", ")}`,
884
+ ].join("\n");
885
+ }
886
+
939
887
  function buildKichiPrompt(): string {
940
888
  return [
941
889
  "Kichi App status sync is available via `kichi_action` and `kichi_clock`.",
@@ -960,7 +908,6 @@ const plugin = {
960
908
 
961
909
  register(api: OpenClawPluginApi) {
962
910
  pluginApi = api;
963
- ensureRuntimeAlbumConfig();
964
911
  registerPluginHooks(api);
965
912
  const musicTitleEnum = getMusicTitleEnum();
966
913
 
@@ -1014,12 +961,7 @@ const plugin = {
1014
961
  (params as { tags?: unknown } | null)?.tags,
1015
962
  );
1016
963
  if (!avatarId) {
1017
- try {
1018
- const identity = JSON.parse(fs.readFileSync(IDENTITY_PATH, "utf-8")) as {
1019
- avatarId?: string;
1020
- };
1021
- avatarId = identity.avatarId;
1022
- } catch {}
964
+ avatarId = service?.readSavedAvatarId() ?? undefined;
1023
965
  }
1024
966
  if (!avatarId) {
1025
967
  return { success: false, error: "No avatarId" };
@@ -1050,6 +992,40 @@ const plugin = {
1050
992
  },
1051
993
  });
1052
994
 
995
+ api.registerTool({
996
+ name: "kichi_switch_env",
997
+ description:
998
+ "Switch Kichi runtime environment to local, dev, or prod and reconnect immediately without restarting the gateway.",
999
+ parameters: {
1000
+ type: "object",
1001
+ properties: {
1002
+ env: {
1003
+ type: "string",
1004
+ description: "Target environment: local, dev, or prod",
1005
+ enum: ["local", "dev", "prod"],
1006
+ },
1007
+ },
1008
+ required: ["env"],
1009
+ },
1010
+ execute: async (_toolCallId, params) => {
1011
+ if (!service) {
1012
+ return { success: false, error: "Kichi service is not initialized" };
1013
+ }
1014
+ const env = (params as { env?: unknown } | null)?.env;
1015
+ if (!isKichiEnv(env)) {
1016
+ return { success: false, error: "env must be one of: local, dev, prod" };
1017
+ }
1018
+
1019
+ const status = await service.switchEnvironment(env);
1020
+ scheduleWorkspacePush();
1021
+ return {
1022
+ success: true,
1023
+ env,
1024
+ status,
1025
+ };
1026
+ },
1027
+ });
1028
+
1053
1029
  api.registerTool({
1054
1030
  name: "kichi_rejoin",
1055
1031
  description:
@@ -1107,8 +1083,7 @@ const plugin = {
1107
1083
 
1108
1084
  api.registerTool({
1109
1085
  name: "kichi_action",
1110
- description:
1111
- "Send an action/pose to Kichi world. Use this for explicit Kichi actions and task lifecycle sync.",
1086
+ description: buildKichiActionDescription(),
1112
1087
  parameters: {
1113
1088
  type: "object",
1114
1089
  properties: {
@@ -1147,7 +1122,7 @@ const plugin = {
1147
1122
  }
1148
1123
 
1149
1124
  const normalizedPoseType = poseType as PoseType;
1150
- const poseActions = loadRuntimeConfig().actions[normalizedPoseType];
1125
+ const poseActions = loadStaticConfig().actions[normalizedPoseType];
1151
1126
  const matched = poseActions.find((entry) => entry.toLowerCase() === action.toLowerCase());
1152
1127
  if (!matched) {
1153
1128
  return {
@@ -1436,7 +1411,7 @@ const plugin = {
1436
1411
  if (normalizedTitles.length === 0) {
1437
1412
  return {
1438
1413
  success: false,
1439
- error: "musicTitles must contain at least one valid track name from album-config",
1414
+ error: "musicTitles must contain at least one valid track name from the static config bundled with the plugin package",
1440
1415
  examples: getMusicTitleExamples(),
1441
1416
  };
1442
1417
  }
@@ -1444,7 +1419,7 @@ const plugin = {
1444
1419
  return {
1445
1420
  success: false,
1446
1421
  error: `Unknown musicTitles: ${invalidTitles.join(", ")}`,
1447
- hint: "Use exact track names from the runtime album config under the user's home directory",
1422
+ hint: "Use exact track names from the static config bundled with the plugin package",
1448
1423
  examples: getMusicTitleExamples(),
1449
1424
  };
1450
1425
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.0.1-alpha.49",
3
+ "version": "0.0.1-alpha.50",
4
4
  "description": "Forward OpenClaw agent events to external WebSocket server for visualization",
5
5
  "type": "module",
6
6
  "main": "index.ts",