@yahaha-studio/kichi-forwarder 0.0.1-alpha.27 → 0.0.1-alpha.28

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.
@@ -12,7 +12,15 @@
12
12
  "Bash(gh api:*)",
13
13
  "WebFetch(domain:docs.openclaw.ai)",
14
14
  "WebFetch(domain:openclawskill.cc)",
15
- "WebFetch(domain:yingtu.ai)"
15
+ "WebFetch(domain:yingtu.ai)",
16
+ "WebFetch(domain:lumadock.com)",
17
+ "WebFetch(domain:dev.to)",
18
+ "WebFetch(domain:www.learnclawdbot.org)",
19
+ "Bash(gh auth status:*)",
20
+ "WebFetch(domain:playbooks.com)",
21
+ "WebFetch(domain:smithery.ai)",
22
+ "WebFetch(domain:lobehub.com)",
23
+ "WebFetch(domain:snyk.io)"
16
24
  ]
17
25
  }
18
26
  }
package/index.ts CHANGED
@@ -2,6 +2,7 @@ import fs from "node:fs";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
4
  import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
5
+ import { DEFAULT_ALBUM_CONFIG } from "./src/album-config.js";
5
6
  import { parse } from "./src/config.js";
6
7
  import { KichiForwarderService } from "./src/service.js";
7
8
  import type {
@@ -58,16 +59,15 @@ const RUNTIME_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "kichi-runtime-config.jso
58
59
  const LEGACY_SKILLS_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "skills-config.json");
59
60
  const IDENTITY_PATH = path.join(KICHI_WORLD_DIR, "identity.json");
60
61
  const MAX_NOTEBOARD_TEXT_LENGTH = 200;
62
+ const MUSIC_TITLE_LOOKUP = new Map(
63
+ DEFAULT_ALBUM_CONFIG.track.map((item) => [item.name.toLowerCase(), item.name] as const),
64
+ );
65
+ const MUSIC_TITLE_EXAMPLES = DEFAULT_ALBUM_CONFIG.track.slice(0, 10).map((item) => item.name);
61
66
  let cachedConfig: KichiRuntimeConfig | null = null;
62
67
  let cachedConfigMtime = 0;
63
68
  let cachedConfigPath = "";
64
69
  let service: KichiForwarderService | null = null;
65
70
  let pluginApi: OpenClawPluginApi | null = null;
66
- let lastKnownStatus: ActionResult = {
67
- poseType: "sit",
68
- action: DEFAULT_ACTIONS.sit[0],
69
- bubble: "Working",
70
- };
71
71
 
72
72
  function sanitizeActions(value: unknown, fallback: string[]): string[] {
73
73
  if (!Array.isArray(value)) {
@@ -135,104 +135,34 @@ function loadRuntimeConfig(): KichiRuntimeConfig {
135
135
  return updateCachedRuntimeConfig(DEFAULT_RUNTIME_CONFIG, null);
136
136
  }
137
137
 
138
- function truncateLog(text: string, maxLen = 150): string {
139
- return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
140
- }
141
-
142
- function truncateInline(text: string, maxLen: number): string {
143
- return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
144
- }
145
-
146
- function prefixLogTimestamp(log: string): string {
147
- const trimmed = log.trim();
148
- if (!trimmed) {
149
- return "";
150
- }
151
- const timestamp = new Date().toISOString().replace("T", " ");
152
- return `[${timestamp}] ${trimmed}`;
153
- }
154
-
155
- function stringifyParamsForLog(value: unknown, maxLen = 220): string {
156
- if (value === undefined) {
157
- return "{}";
158
- }
159
- try {
160
- return truncateInline(JSON.stringify(value), maxLen);
161
- } catch {
162
- return truncateInline(String(value), maxLen);
163
- }
164
- }
165
-
166
- function rememberStatus(status: ActionResult): void {
167
- lastKnownStatus = {
168
- poseType: status.poseType,
169
- action: status.action,
170
- bubble: status.bubble.trim() || status.action,
171
- };
172
- }
173
-
174
- function sendStatusAndRemember(status: ActionResult, log: string): void {
175
- rememberStatus(status);
138
+ function sendStatusUpdate(status: ActionResult): void {
176
139
  service?.sendStatus(
177
140
  status.poseType,
178
141
  status.action,
179
142
  status.bubble || status.action,
180
- prefixLogTimestamp(log),
143
+ typeof status.log === "string" ? status.log.trim() : "",
181
144
  );
182
145
  }
183
146
 
184
- function forwardToolCallLog(toolName: string, params: unknown, agentId?: string): void {
185
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
186
- return;
187
- }
188
-
189
- if (!toolName || toolName === "kichi_action") {
190
- return;
191
- }
192
-
193
- const paramsText = stringifyParamsForLog(params);
194
- const bubble = lastKnownStatus.bubble.trim() || lastKnownStatus.action;
195
- const prefix = typeof agentId === "string" && agentId.trim() ? `[${agentId.trim()}] ` : "";
196
- const log = truncateLog(`${prefix}exec tool: ${toolName}, params: ${paramsText}`, 300);
197
- service.sendStatus(lastKnownStatus.poseType, lastKnownStatus.action, bubble, prefixLogTimestamp(log));
198
- }
199
-
200
- function resolveStatusSourceId(ctx?: { agentId?: string; sessionKey?: string }): string | undefined {
201
- if (typeof ctx?.agentId === "string" && ctx.agentId.trim()) {
202
- return ctx.agentId.trim();
203
- }
204
- if (typeof ctx?.sessionKey === "string" && ctx.sessionKey.trim()) {
205
- return ctx.sessionKey.trim();
206
- }
207
- return undefined;
208
- }
209
-
210
147
  function isLlmRuntimeEnabled(): boolean {
211
148
  return loadRuntimeConfig().llmRuntimeEnabled;
212
149
  }
213
150
 
214
- function syncFixedStatus(status: ActionResult, log = ""): void {
151
+ function syncFixedStatus(status: ActionResult): void {
215
152
  if (!service?.hasValidIdentity() || !service?.isConnected()) {
216
153
  return;
217
154
  }
218
- sendStatusAndRemember(status, log);
219
- }
220
-
221
- function buildToolExecutionLog(toolName: string, params: unknown, agentId?: string): string {
222
- const paramsText = stringifyParamsForLog(params);
223
- const prefix = typeof agentId === "string" && agentId.trim() ? `[${agentId.trim()}] ` : "";
224
- return truncateLog(`${prefix}exec tool: ${toolName}, params: ${paramsText}`, 300);
155
+ const bubbleText = status.bubble.trim() || status.action;
156
+ sendStatusUpdate({
157
+ ...status,
158
+ bubble: bubbleText,
159
+ log: bubbleText,
160
+ });
225
161
  }
226
162
 
227
- async function handleMessageReceivedHook(
228
- event: { content?: string },
229
- _ctx?: { agentId?: string; sessionKey?: string },
230
- ): Promise<void> {
163
+ async function handleMessageReceivedHook(): Promise<void> {
231
164
  if (!isLlmRuntimeEnabled()) {
232
- const preview = typeof event?.content === "string" && event.content.trim()
233
- ? truncateLog(`message received: ${event.content.trim()}`, 220)
234
- : "message received";
235
- syncFixedStatus(FIXED_HOOK_STATUSES.messageReceived, preview);
165
+ syncFixedStatus(FIXED_HOOK_STATUSES.messageReceived);
236
166
  }
237
167
  return;
238
168
  }
@@ -251,31 +181,21 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
251
181
  };
252
182
  });
253
183
 
254
- api.on("before_tool_call", (event, ctx) => {
184
+ api.on("before_tool_call", () => {
255
185
  if (!isLlmRuntimeEnabled()) {
256
- syncFixedStatus(
257
- FIXED_HOOK_STATUSES.beforeToolCall,
258
- buildToolExecutionLog(event.toolName, event.params, ctx?.agentId),
259
- );
260
- return;
186
+ syncFixedStatus(FIXED_HOOK_STATUSES.beforeToolCall);
261
187
  }
262
- forwardToolCallLog(event.toolName, event.params, ctx?.agentId);
263
188
  });
264
189
 
265
- api.on("message_received", async (event, ctx) => {
266
- await handleMessageReceivedHook(event, ctx);
190
+ api.on("message_received", async () => {
191
+ await handleMessageReceivedHook();
267
192
  });
268
193
 
269
194
  api.on("agent_end", (event) => {
270
195
  if (isLlmRuntimeEnabled()) {
271
196
  return;
272
197
  }
273
- syncFixedStatus(
274
- event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure,
275
- event.success
276
- ? "task finished"
277
- : truncateLog(`task failed: ${event.error ?? "unknown error"}`, 220),
278
- );
198
+ syncFixedStatus(event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure);
279
199
  });
280
200
  }
281
201
 
@@ -291,6 +211,36 @@ function isPositiveInteger(value: unknown): value is number {
291
211
  return typeof value === "number" && Number.isInteger(value) && value > 0;
292
212
  }
293
213
 
214
+ function normalizeJoinTags(value: unknown): { tags?: string[]; error?: string } {
215
+ if (value === undefined) {
216
+ return { tags: [] };
217
+ }
218
+ if (!Array.isArray(value)) {
219
+ return { error: "tags must be an array of strings" };
220
+ }
221
+
222
+ const tags: string[] = [];
223
+ const seen = new Set<string>();
224
+
225
+ for (const item of value) {
226
+ if (typeof item !== "string") {
227
+ return { error: "tags must be an array of strings" };
228
+ }
229
+ const trimmed = item.trim();
230
+ if (!trimmed) {
231
+ continue;
232
+ }
233
+ const key = trimmed.toLowerCase();
234
+ if (seen.has(key)) {
235
+ continue;
236
+ }
237
+ seen.add(key);
238
+ tags.push(trimmed);
239
+ }
240
+
241
+ return { tags };
242
+ }
243
+
294
244
  function isClockAction(value: unknown): value is ClockAction {
295
245
  return ["set", "stop"].includes(String(value));
296
246
  }
@@ -418,6 +368,40 @@ function pickRandomAction(actions: string[]): string {
418
368
  return actions[Math.floor(Math.random() * actions.length)];
419
369
  }
420
370
 
371
+ function normalizeMusicTitles(value: unknown): { titles: string[]; invalidTitles: string[] } {
372
+ if (!Array.isArray(value)) {
373
+ return { titles: [], invalidTitles: [] };
374
+ }
375
+
376
+ const titles: string[] = [];
377
+ const invalidTitles: string[] = [];
378
+ const seen = new Set<string>();
379
+
380
+ for (const item of value) {
381
+ if (typeof item !== "string") {
382
+ invalidTitles.push(String(item));
383
+ continue;
384
+ }
385
+ const trimmed = item.trim();
386
+ if (!trimmed) {
387
+ continue;
388
+ }
389
+
390
+ const key = trimmed.toLowerCase();
391
+ const canonicalTitle = MUSIC_TITLE_LOOKUP.get(key);
392
+ if (!canonicalTitle) {
393
+ invalidTitles.push(trimmed);
394
+ continue;
395
+ }
396
+ if (seen.has(key)) {
397
+ continue;
398
+ }
399
+ seen.add(key);
400
+ titles.push(canonicalTitle);
401
+ }
402
+
403
+ return { titles, invalidTitles };
404
+ }
421
405
 
422
406
  function buildKichiPrompt(): string {
423
407
  return [
@@ -430,6 +414,8 @@ function buildKichiPrompt(): string {
430
414
  "- Task end (highest priority): Before the final user-visible reply of this turn, MUST call `kichi_action` exactly once",
431
415
  "- Required order at task end: 1) call `kichi_action` 2) send final reply",
432
416
  "- Trivial-operation skip applies only to Task start / Step switch / Task switch, NOT Task end",
417
+ "- `bubble`: short natural companion speech, not a raw status report",
418
+ "- `log`: optional first-person diary-style note about the current operation, action, status, mood, feeling, or feedback; keep it within 20 words",
433
419
  "",
434
420
  "When to use `kichi_clock`:",
435
421
  "- For tasks with 2+ meaningful steps or work likely to take more than a brief moment (~10s), set a `countDown` at task start.",
@@ -437,6 +423,11 @@ function buildKichiPrompt(): string {
437
423
  "- If duration is uncertain, start with a reasonable estimate and adjust as work progresses.",
438
424
  "- If user requests a timer style, follow it (`pomodoro`, `countDown`, or `countUp`).",
439
425
  "",
426
+ "When to use `kichi_music_album_create`:",
427
+ "- Call `kichi_query_status` first.",
428
+ "- Recommend a variable-length playlist based on weather, time, and your own personality.",
429
+ "- `albumTitle` is user-defined and `musicTitles` must be exact track names from album-config.",
430
+ "",
440
431
  "Skip all sync if:",
441
432
  "- User says 'don't sync to Kichi' or similar",
442
433
  "- Task is only about configuring/testing kichi_* tools",
@@ -469,11 +460,11 @@ const plugin = {
469
460
 
470
461
  api.registerTool({
471
462
  name: "kichi_join",
472
- description: "Join Kichi world with mateId, the current bot name, and a short bio",
463
+ description: "Join Kichi world with avatarId, the current bot name, a short bio, and personality tags",
473
464
  parameters: {
474
465
  type: "object",
475
466
  properties: {
476
- mateId: { type: "string", description: "Mate ID to join Kichi world" },
467
+ avatarId: { type: "string", description: "Avatar ID to join Kichi world" },
477
468
  botName: {
478
469
  type: "string",
479
470
  description: "Current bot name to include in the join message",
@@ -482,23 +473,31 @@ const plugin = {
482
473
  type: "string",
483
474
  description: "Short bio covering OpenClaw personality and role",
484
475
  },
476
+ tags: {
477
+ type: "array",
478
+ description: "Optional list of OpenClaw self-perceived personality tags",
479
+ items: { type: "string" },
480
+ },
485
481
  },
486
482
  required: ["botName", "bio"],
487
483
  },
488
484
  execute: async (_toolCallId, params) => {
489
- let mateId = (params as { mateId?: string } | null)?.mateId;
485
+ let avatarId = (params as { avatarId?: string } | null)?.avatarId;
490
486
  const botName = (params as { botName?: string } | null)?.botName?.trim();
491
487
  const bio = (params as { bio?: string } | null)?.bio?.trim();
492
- if (!mateId) {
488
+ const { tags, error: tagsError } = normalizeJoinTags(
489
+ (params as { tags?: unknown } | null)?.tags,
490
+ );
491
+ if (!avatarId) {
493
492
  try {
494
493
  const identity = JSON.parse(fs.readFileSync(IDENTITY_PATH, "utf-8")) as {
495
- mateId?: string;
494
+ avatarId?: string;
496
495
  };
497
- mateId = identity.mateId;
496
+ avatarId = identity.avatarId;
498
497
  } catch {}
499
498
  }
500
- if (!mateId) {
501
- return { success: false, error: "No mateId" };
499
+ if (!avatarId) {
500
+ return { success: false, error: "No avatarId" };
502
501
  }
503
502
  if (!botName) {
504
503
  return { success: false, error: "No botName" };
@@ -506,7 +505,10 @@ const plugin = {
506
505
  if (!bio) {
507
506
  return { success: false, error: "No bio" };
508
507
  }
509
- const result = await service?.join(mateId, botName, bio);
508
+ if (tagsError) {
509
+ return { success: false, error: tagsError };
510
+ }
511
+ const result = await service?.join(avatarId, botName, bio, tags ?? []);
510
512
  return result ? { success: true, authKey: result } : { success: false, error: "Failed" };
511
513
  },
512
514
  });
@@ -514,7 +516,7 @@ const plugin = {
514
516
  api.registerTool({
515
517
  name: "kichi_rejoin",
516
518
  description:
517
- "Request an immediate rejoin attempt with saved mateId/authKey. Rejoin is also sent automatically after reconnect.",
519
+ "Request an immediate rejoin attempt with saved avatarId/authKey. Rejoin is also sent automatically after reconnect.",
518
520
  parameters: { type: "object", properties: {} },
519
521
  execute: async () => {
520
522
  if (!service) {
@@ -568,14 +570,20 @@ const plugin = {
568
570
  description: "Action name (for example High Five or Typing with Keyboard)",
569
571
  },
570
572
  bubble: { type: "string", description: "Optional bubble text to display (max 5 words)" },
573
+ log: {
574
+ type: "string",
575
+ description:
576
+ "Optional first-person log about the current operation, action, status, mood, or feedback (max 20 words)",
577
+ },
571
578
  },
572
579
  required: ["poseType", "action"],
573
580
  },
574
581
  execute: async (_toolCallId, params) => {
575
- const { poseType, action, bubble } = (params || {}) as {
582
+ const { poseType, action, bubble, log } = (params || {}) as {
576
583
  poseType?: string;
577
584
  action?: string;
578
585
  bubble?: string;
586
+ log?: string;
579
587
  };
580
588
  if (!poseType || !action) {
581
589
  return { success: false, error: "poseType and action parameters are required" };
@@ -602,20 +610,21 @@ const plugin = {
602
610
  }
603
611
 
604
612
  const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched;
605
- // Keep explicit kichi_action sync free of tool/log noise.
606
- sendStatusAndRemember(
613
+ const logText = typeof log === "string" ? log.trim() : "";
614
+ sendStatusUpdate(
607
615
  {
608
616
  poseType: normalizedPoseType,
609
617
  action: matched,
610
618
  bubble: bubbleText,
619
+ log: logText,
611
620
  },
612
- "",
613
621
  );
614
622
  return {
615
623
  success: true,
616
624
  poseType: normalizedPoseType,
617
625
  action: matched,
618
626
  bubble: bubbleText,
627
+ log: logText,
619
628
  };
620
629
  },
621
630
  });
@@ -735,7 +744,7 @@ const plugin = {
735
744
  api.registerTool({
736
745
  name: "kichi_query_status",
737
746
  description:
738
- "Query Kichi note boards for the current mate. Use this before creating a new note, especially when you may want to relate it to an existing note.",
747
+ "Query Kichi avatar status (notes, weather/time, timer snapshot, and daily note quota). Use this before creating a new note.",
739
748
  parameters: {
740
749
  type: "object",
741
750
  properties: {
@@ -755,14 +764,102 @@ const plugin = {
755
764
  }
756
765
 
757
766
  try {
758
- const result = await service.queryNotesBoard(
767
+ const result = await service.queryStatus(
759
768
  typeof requestId === "string" ? requestId : undefined,
760
769
  );
761
770
  return result;
762
771
  } catch (error) {
763
772
  return {
764
773
  success: false,
765
- error: `Failed to query note boards: ${error}`,
774
+ error: `Failed to query status: ${error}`,
775
+ };
776
+ }
777
+ },
778
+ });
779
+
780
+ api.registerTool({
781
+ name: "kichi_music_album_create",
782
+ description:
783
+ "Create a custom Kichi music album. Query status first, then choose track names from album-config that match weather/time and personality.",
784
+ parameters: {
785
+ type: "object",
786
+ properties: {
787
+ requestId: {
788
+ type: "string",
789
+ description: "Optional request ID for tracing or deduplication.",
790
+ },
791
+ albumTitle: {
792
+ type: "string",
793
+ description: "Custom album title.",
794
+ },
795
+ musicTitles: {
796
+ type: "array",
797
+ description: "Track names chosen from album-config.",
798
+ items: {
799
+ type: "string",
800
+ },
801
+ },
802
+ },
803
+ required: ["albumTitle", "musicTitles"],
804
+ },
805
+ execute: async (_toolCallId, params) => {
806
+ const {
807
+ requestId,
808
+ albumTitle,
809
+ musicTitles,
810
+ } = (params || {}) as {
811
+ requestId?: unknown;
812
+ albumTitle?: unknown;
813
+ musicTitles?: unknown;
814
+ };
815
+
816
+ if (requestId !== undefined && typeof requestId !== "string") {
817
+ return { success: false, error: "requestId must be a string when provided" };
818
+ }
819
+ if (typeof albumTitle !== "string" || !albumTitle.trim()) {
820
+ return { success: false, error: "albumTitle is required" };
821
+ }
822
+ if (!Array.isArray(musicTitles)) {
823
+ return { success: false, error: "musicTitles must be an array of track names" };
824
+ }
825
+
826
+ const { titles: normalizedTitles, invalidTitles } = normalizeMusicTitles(musicTitles);
827
+ if (normalizedTitles.length === 0) {
828
+ return {
829
+ success: false,
830
+ error: "musicTitles must contain at least one valid track name from album-config",
831
+ examples: MUSIC_TITLE_EXAMPLES,
832
+ };
833
+ }
834
+ if (invalidTitles.length > 0) {
835
+ return {
836
+ success: false,
837
+ error: `Unknown musicTitles: ${invalidTitles.join(", ")}`,
838
+ hint: "Use exact track names from src/album-config.ts",
839
+ examples: MUSIC_TITLE_EXAMPLES,
840
+ };
841
+ }
842
+ if (!service?.hasValidIdentity() || !service?.isConnected()) {
843
+ return { success: false, error: "Not connected to Kichi world" };
844
+ }
845
+
846
+ try {
847
+ const normalizedRequestId = service.createMusicAlbum(
848
+ albumTitle.trim(),
849
+ normalizedTitles,
850
+ typeof requestId === "string" ? requestId : undefined,
851
+ );
852
+ return {
853
+ success: true,
854
+ requestId: normalizedRequestId,
855
+ albumTitle: albumTitle.trim(),
856
+ musicTitles: normalizedTitles,
857
+ trackCount: normalizedTitles.length,
858
+ };
859
+ } catch (error) {
860
+ return {
861
+ success: false,
862
+ error: `Failed to create music album: ${error}`,
766
863
  };
767
864
  }
768
865
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.0.1-alpha.27",
3
+ "version": "0.0.1-alpha.28",
4
4
  "description": "Forward OpenClaw agent events to external WebSocket server for visualization",
5
5
  "type": "module",
6
6
  "main": "index.ts",