@yahaha-studio/kichi-forwarder 0.0.1-alpha.26 → 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,14 +2,13 @@ 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 {
8
9
  ActionResult,
9
10
  ClockAction,
10
11
  ClockConfig,
11
- CreateNotesBoardNote,
12
- CreateNotesBoardNoteResultPayload,
13
12
  KichiRuntimeConfig,
14
13
  KichiForwarderConfig,
15
14
  PomodoroPhase,
@@ -25,6 +24,34 @@ const DEFAULT_ACTIONS: KichiRuntimeConfig["actions"] = {
25
24
 
26
25
  const DEFAULT_RUNTIME_CONFIG: KichiRuntimeConfig = {
27
26
  actions: DEFAULT_ACTIONS,
27
+ llmRuntimeEnabled: true,
28
+ };
29
+ const FIXED_HOOK_STATUSES: Record<string, ActionResult> = {
30
+ messageReceived: {
31
+ poseType: "sit",
32
+ action: "Study Look At",
33
+ bubble: "Reading request",
34
+ },
35
+ beforePromptBuild: {
36
+ poseType: "sit",
37
+ action: "Thinking",
38
+ bubble: "Planning task",
39
+ },
40
+ beforeToolCall: {
41
+ poseType: "sit",
42
+ action: "Typing with Keyboard",
43
+ bubble: "Working step",
44
+ },
45
+ agentEndSuccess: {
46
+ poseType: "stand",
47
+ action: "Yay",
48
+ bubble: "Task complete",
49
+ },
50
+ agentEndFailure: {
51
+ poseType: "stand",
52
+ action: "Tired",
53
+ bubble: "Task failed",
54
+ },
28
55
  };
29
56
 
30
57
  const KICHI_WORLD_DIR = path.join(os.homedir(), ".openclaw", "kichi-world");
@@ -32,16 +59,15 @@ const RUNTIME_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "kichi-runtime-config.jso
32
59
  const LEGACY_SKILLS_CONFIG_PATH = path.join(KICHI_WORLD_DIR, "skills-config.json");
33
60
  const IDENTITY_PATH = path.join(KICHI_WORLD_DIR, "identity.json");
34
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);
35
66
  let cachedConfig: KichiRuntimeConfig | null = null;
36
67
  let cachedConfigMtime = 0;
37
68
  let cachedConfigPath = "";
38
69
  let service: KichiForwarderService | null = null;
39
70
  let pluginApi: OpenClawPluginApi | null = null;
40
- let lastKnownStatus: ActionResult = {
41
- poseType: "sit",
42
- action: DEFAULT_ACTIONS.sit[0],
43
- bubble: "Working",
44
- };
45
71
 
46
72
  function sanitizeActions(value: unknown, fallback: string[]): string[] {
47
73
  if (!Array.isArray(value)) {
@@ -57,6 +83,7 @@ function normalizeRuntimeConfig(value: unknown): KichiRuntimeConfig {
57
83
  const raw = value && typeof value === "object" ? (value as Partial<KichiRuntimeConfig>) : {};
58
84
  const actions = raw.actions;
59
85
  return {
86
+ llmRuntimeEnabled: typeof raw.llmRuntimeEnabled === "boolean" ? raw.llmRuntimeEnabled : true,
60
87
  actions: {
61
88
  stand: sanitizeActions(actions?.stand, DEFAULT_ACTIONS.stand),
62
89
  sit: sanitizeActions(actions?.sit, DEFAULT_ACTIONS.sit),
@@ -108,82 +135,35 @@ function loadRuntimeConfig(): KichiRuntimeConfig {
108
135
  return updateCachedRuntimeConfig(DEFAULT_RUNTIME_CONFIG, null);
109
136
  }
110
137
 
111
- function truncateLog(text: string, maxLen = 150): string {
112
- return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
113
- }
114
-
115
- function truncateInline(text: string, maxLen: number): string {
116
- return text.length > maxLen ? `${text.slice(0, maxLen)}...` : text;
117
- }
118
-
119
- function prefixLogTimestamp(log: string): string {
120
- const trimmed = log.trim();
121
- if (!trimmed) {
122
- return "";
123
- }
124
- const timestamp = new Date().toISOString().replace("T", " ");
125
- return `[${timestamp}] ${trimmed}`;
126
- }
127
-
128
- function stringifyParamsForLog(value: unknown, maxLen = 220): string {
129
- if (value === undefined) {
130
- return "{}";
131
- }
132
- try {
133
- return truncateInline(JSON.stringify(value), maxLen);
134
- } catch {
135
- return truncateInline(String(value), maxLen);
136
- }
137
- }
138
-
139
- function rememberStatus(status: ActionResult): void {
140
- lastKnownStatus = {
141
- poseType: status.poseType,
142
- action: status.action,
143
- bubble: status.bubble.trim() || status.action,
144
- };
145
- }
146
-
147
- function sendStatusAndRemember(status: ActionResult, log: string): void {
148
- rememberStatus(status);
138
+ function sendStatusUpdate(status: ActionResult): void {
149
139
  service?.sendStatus(
150
140
  status.poseType,
151
141
  status.action,
152
142
  status.bubble || status.action,
153
- prefixLogTimestamp(log),
143
+ typeof status.log === "string" ? status.log.trim() : "",
154
144
  );
155
145
  }
156
146
 
157
- function forwardToolCallLog(toolName: string, params: unknown, agentId?: string): void {
158
- if (!service?.hasValidIdentity() || !service?.isConnected()) {
159
- return;
160
- }
147
+ function isLlmRuntimeEnabled(): boolean {
148
+ return loadRuntimeConfig().llmRuntimeEnabled;
149
+ }
161
150
 
162
- if (!toolName || toolName === "kichi_action") {
151
+ function syncFixedStatus(status: ActionResult): void {
152
+ if (!service?.hasValidIdentity() || !service?.isConnected()) {
163
153
  return;
164
154
  }
165
-
166
- const paramsText = stringifyParamsForLog(params);
167
- const bubble = lastKnownStatus.bubble.trim() || lastKnownStatus.action;
168
- const prefix = typeof agentId === "string" && agentId.trim() ? `[${agentId.trim()}] ` : "";
169
- const log = truncateLog(`${prefix}exec tool: ${toolName}, params: ${paramsText}`, 300);
170
- service.sendStatus(lastKnownStatus.poseType, lastKnownStatus.action, bubble, prefixLogTimestamp(log));
155
+ const bubbleText = status.bubble.trim() || status.action;
156
+ sendStatusUpdate({
157
+ ...status,
158
+ bubble: bubbleText,
159
+ log: bubbleText,
160
+ });
171
161
  }
172
162
 
173
- function resolveStatusSourceId(ctx?: { agentId?: string; sessionKey?: string }): string | undefined {
174
- if (typeof ctx?.agentId === "string" && ctx.agentId.trim()) {
175
- return ctx.agentId.trim();
176
- }
177
- if (typeof ctx?.sessionKey === "string" && ctx.sessionKey.trim()) {
178
- return ctx.sessionKey.trim();
163
+ async function handleMessageReceivedHook(): Promise<void> {
164
+ if (!isLlmRuntimeEnabled()) {
165
+ syncFixedStatus(FIXED_HOOK_STATUSES.messageReceived);
179
166
  }
180
- return undefined;
181
- }
182
-
183
- async function handleMessageReceivedHook(
184
- _event: any,
185
- _ctx?: { agentId?: string; sessionKey?: string },
186
- ): Promise<void> {
187
167
  return;
188
168
  }
189
169
 
@@ -192,17 +172,30 @@ function registerPluginHooks(api: OpenClawPluginApi): void {
192
172
  if (!service?.hasValidIdentity() || !service?.isConnected()) {
193
173
  return;
194
174
  }
175
+ if (!isLlmRuntimeEnabled()) {
176
+ syncFixedStatus(FIXED_HOOK_STATUSES.beforePromptBuild);
177
+ return;
178
+ }
195
179
  return {
196
180
  prependContext: buildKichiPrompt(),
197
181
  };
198
182
  });
199
183
 
200
- api.on("before_tool_call", (event, ctx) => {
201
- forwardToolCallLog(event.toolName, event.params, ctx?.agentId);
184
+ api.on("before_tool_call", () => {
185
+ if (!isLlmRuntimeEnabled()) {
186
+ syncFixedStatus(FIXED_HOOK_STATUSES.beforeToolCall);
187
+ }
202
188
  });
203
189
 
204
- api.on("message_received", async (event, ctx) => {
205
- await handleMessageReceivedHook(event, ctx);
190
+ api.on("message_received", async () => {
191
+ await handleMessageReceivedHook();
192
+ });
193
+
194
+ api.on("agent_end", (event) => {
195
+ if (isLlmRuntimeEnabled()) {
196
+ return;
197
+ }
198
+ syncFixedStatus(event.success ? FIXED_HOOK_STATUSES.agentEndSuccess : FIXED_HOOK_STATUSES.agentEndFailure);
206
199
  });
207
200
  }
208
201
 
@@ -218,6 +211,36 @@ function isPositiveInteger(value: unknown): value is number {
218
211
  return typeof value === "number" && Number.isInteger(value) && value > 0;
219
212
  }
220
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
+
221
244
  function isClockAction(value: unknown): value is ClockAction {
222
245
  return ["set", "stop"].includes(String(value));
223
246
  }
@@ -345,44 +368,39 @@ function pickRandomAction(actions: string[]): string {
345
368
  return actions[Math.floor(Math.random() * actions.length)];
346
369
  }
347
370
 
348
- function truncateNoteData(
349
- data: string,
350
- maxLen = 500,
351
- ): { data: string; dataTruncated: boolean } {
352
- if (data.length <= maxLen) {
353
- return { data, dataTruncated: false };
371
+ function normalizeMusicTitles(value: unknown): { titles: string[]; invalidTitles: string[] } {
372
+ if (!Array.isArray(value)) {
373
+ return { titles: [], invalidTitles: [] };
354
374
  }
355
- return { data: `${data.slice(0, maxLen)}...`, dataTruncated: true };
356
- }
357
375
 
358
- function summarizeCreatedNote(note: CreateNotesBoardNote) {
359
- const { data, dataTruncated } = truncateNoteData(note.data);
360
- return {
361
- id: note.id,
362
- ownerName: note.ownerName,
363
- createTime: note.createTime,
364
- data,
365
- dataTruncated,
366
- };
367
- }
376
+ const titles: string[] = [];
377
+ const invalidTitles: string[] = [];
378
+ const seen = new Set<string>();
368
379
 
369
- function buildMutationSummary(result: CreateNotesBoardNoteResultPayload): string {
370
- if (!result.success) {
371
- const parts = ["Mutation failed"];
372
- if ("errorCode" in result && result.errorCode) {
373
- parts.push(`error=${result.errorCode}`);
380
+ for (const item of value) {
381
+ if (typeof item !== "string") {
382
+ invalidTitles.push(String(item));
383
+ continue;
374
384
  }
375
- if (typeof result.remaining === "number") {
376
- parts.push(`remaining=${result.remaining}`);
385
+ const trimmed = item.trim();
386
+ if (!trimmed) {
387
+ continue;
377
388
  }
378
- if (result.resetAtUtc) {
379
- parts.push(`resetAtUtc=${result.resetAtUtc}`);
389
+
390
+ const key = trimmed.toLowerCase();
391
+ const canonicalTitle = MUSIC_TITLE_LOOKUP.get(key);
392
+ if (!canonicalTitle) {
393
+ invalidTitles.push(trimmed);
394
+ continue;
380
395
  }
381
- return parts.join(", ");
396
+ if (seen.has(key)) {
397
+ continue;
398
+ }
399
+ seen.add(key);
400
+ titles.push(canonicalTitle);
382
401
  }
383
402
 
384
- const text = truncateNoteData(result.note.data, 120).data.replace(/\s+/g, " ").trim();
385
- return `${result.propId} -> ${result.note.id} by ${result.note.ownerName}: ${text}`;
403
+ return { titles, invalidTitles };
386
404
  }
387
405
 
388
406
  function buildKichiPrompt(): string {
@@ -391,23 +409,24 @@ function buildKichiPrompt(): string {
391
409
  "",
392
410
  "When to use `kichi_action`:",
393
411
  "- Task start: User gives you a new task to work on",
412
+ "- Step switch: Moving from one meaningful step or subtask to another within the current task",
394
413
  "- Task switch: Moving from one distinct task to another",
395
- "- Major milestone: Completed a significant phase",
396
414
  "- Task end (highest priority): Before the final user-visible reply of this turn, MUST call `kichi_action` exactly once",
397
415
  "- Required order at task end: 1) call `kichi_action` 2) send final reply",
398
- "- Trivial-operation skip applies only to Task start / Task switch / Major milestone, NOT Task end",
399
- "",
400
- "How to choose parameters:",
401
- "- Choose poseType, action, and bubble that match your actual current activity",
402
- "- Use available actions from the configured action list for each poseType",
403
- "- bubble should be 2-5 words describing what you're doing now",
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",
404
419
  "",
405
420
  "When to use `kichi_clock`:",
406
- "- Default behavior: For most tasks, set a `countDown` at task start to plan your time.",
407
- "- Prefer using it whenever work has 2+ steps or is likely to take more than a brief moment (~10s).",
421
+ "- For tasks with 2+ meaningful steps or work likely to take more than a brief moment (~10s), set a `countDown` at task start.",
422
+ "- Skip clock only for truly quick one-shot operations.",
408
423
  "- If duration is uncertain, start with a reasonable estimate and adjust as work progresses.",
409
424
  "- If user requests a timer style, follow it (`pomodoro`, `countDown`, or `countUp`).",
410
- "- Skip only for truly quick one-shot operations (for example a single file read or one simple command).",
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.",
411
430
  "",
412
431
  "Skip all sync if:",
413
432
  "- User says 'don't sync to Kichi' or similar",
@@ -441,11 +460,11 @@ const plugin = {
441
460
 
442
461
  api.registerTool({
443
462
  name: "kichi_join",
444
- 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",
445
464
  parameters: {
446
465
  type: "object",
447
466
  properties: {
448
- mateId: { type: "string", description: "Mate ID to join Kichi world" },
467
+ avatarId: { type: "string", description: "Avatar ID to join Kichi world" },
449
468
  botName: {
450
469
  type: "string",
451
470
  description: "Current bot name to include in the join message",
@@ -454,23 +473,31 @@ const plugin = {
454
473
  type: "string",
455
474
  description: "Short bio covering OpenClaw personality and role",
456
475
  },
476
+ tags: {
477
+ type: "array",
478
+ description: "Optional list of OpenClaw self-perceived personality tags",
479
+ items: { type: "string" },
480
+ },
457
481
  },
458
482
  required: ["botName", "bio"],
459
483
  },
460
484
  execute: async (_toolCallId, params) => {
461
- let mateId = (params as { mateId?: string } | null)?.mateId;
485
+ let avatarId = (params as { avatarId?: string } | null)?.avatarId;
462
486
  const botName = (params as { botName?: string } | null)?.botName?.trim();
463
487
  const bio = (params as { bio?: string } | null)?.bio?.trim();
464
- if (!mateId) {
488
+ const { tags, error: tagsError } = normalizeJoinTags(
489
+ (params as { tags?: unknown } | null)?.tags,
490
+ );
491
+ if (!avatarId) {
465
492
  try {
466
493
  const identity = JSON.parse(fs.readFileSync(IDENTITY_PATH, "utf-8")) as {
467
- mateId?: string;
494
+ avatarId?: string;
468
495
  };
469
- mateId = identity.mateId;
496
+ avatarId = identity.avatarId;
470
497
  } catch {}
471
498
  }
472
- if (!mateId) {
473
- return { success: false, error: "No mateId" };
499
+ if (!avatarId) {
500
+ return { success: false, error: "No avatarId" };
474
501
  }
475
502
  if (!botName) {
476
503
  return { success: false, error: "No botName" };
@@ -478,7 +505,10 @@ const plugin = {
478
505
  if (!bio) {
479
506
  return { success: false, error: "No bio" };
480
507
  }
481
- 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 ?? []);
482
512
  return result ? { success: true, authKey: result } : { success: false, error: "Failed" };
483
513
  },
484
514
  });
@@ -486,7 +516,7 @@ const plugin = {
486
516
  api.registerTool({
487
517
  name: "kichi_rejoin",
488
518
  description:
489
- "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.",
490
520
  parameters: { type: "object", properties: {} },
491
521
  execute: async () => {
492
522
  if (!service) {
@@ -540,14 +570,20 @@ const plugin = {
540
570
  description: "Action name (for example High Five or Typing with Keyboard)",
541
571
  },
542
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
+ },
543
578
  },
544
579
  required: ["poseType", "action"],
545
580
  },
546
581
  execute: async (_toolCallId, params) => {
547
- const { poseType, action, bubble } = (params || {}) as {
582
+ const { poseType, action, bubble, log } = (params || {}) as {
548
583
  poseType?: string;
549
584
  action?: string;
550
585
  bubble?: string;
586
+ log?: string;
551
587
  };
552
588
  if (!poseType || !action) {
553
589
  return { success: false, error: "poseType and action parameters are required" };
@@ -574,20 +610,21 @@ const plugin = {
574
610
  }
575
611
 
576
612
  const bubbleText = typeof bubble === "string" && bubble.trim() ? bubble.trim() : matched;
577
- // Keep explicit kichi_action sync free of tool/log noise.
578
- sendStatusAndRemember(
613
+ const logText = typeof log === "string" ? log.trim() : "";
614
+ sendStatusUpdate(
579
615
  {
580
616
  poseType: normalizedPoseType,
581
617
  action: matched,
582
618
  bubble: bubbleText,
619
+ log: logText,
583
620
  },
584
- "",
585
621
  );
586
622
  return {
587
623
  success: true,
588
624
  poseType: normalizedPoseType,
589
625
  action: matched,
590
626
  bubble: bubbleText,
627
+ log: logText,
591
628
  };
592
629
  },
593
630
  });
@@ -705,9 +742,9 @@ const plugin = {
705
742
  });
706
743
 
707
744
  api.registerTool({
708
- name: "kichi_noteboard_query",
745
+ name: "kichi_query_status",
709
746
  description:
710
- "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.",
711
748
  parameters: {
712
749
  type: "object",
713
750
  properties: {
@@ -727,14 +764,102 @@ const plugin = {
727
764
  }
728
765
 
729
766
  try {
730
- const result = await service.queryNotesBoard(
767
+ const result = await service.queryStatus(
731
768
  typeof requestId === "string" ? requestId : undefined,
732
769
  );
733
770
  return result;
734
771
  } catch (error) {
735
772
  return {
736
773
  success: false,
737
- 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}`,
738
863
  };
739
864
  }
740
865
  },
@@ -755,18 +880,13 @@ const plugin = {
755
880
  type: "string",
756
881
  description: "Note content to create. Maximum 200 characters.",
757
882
  },
758
- requestId: {
759
- type: "string",
760
- description: "Optional request ID for tracing or deduplication.",
761
- },
762
883
  },
763
884
  required: ["propId", "data"],
764
885
  },
765
886
  execute: async (_toolCallId, params) => {
766
- const { propId, data, requestId } = (params || {}) as {
887
+ const { propId, data } = (params || {}) as {
767
888
  propId?: unknown;
768
889
  data?: unknown;
769
- requestId?: unknown;
770
890
  };
771
891
  if (typeof propId !== "string" || !propId.trim()) {
772
892
  return { success: false, error: "propId is required" };
@@ -780,38 +900,13 @@ const plugin = {
780
900
  error: `data must be ${MAX_NOTEBOARD_TEXT_LENGTH} characters or fewer`,
781
901
  };
782
902
  }
783
- if (requestId !== undefined && typeof requestId !== "string") {
784
- return { success: false, error: "requestId must be a string when provided" };
785
- }
786
903
  if (!service?.hasValidIdentity() || !service?.isConnected()) {
787
904
  return { success: false, error: "Not connected to Kichi world" };
788
905
  }
789
906
 
790
907
  try {
791
- const result = await service.createNotesBoardNote(
792
- propId.trim(),
793
- data.trim(),
794
- typeof requestId === "string" ? requestId : undefined,
795
- );
796
- if (!result.success) {
797
- return {
798
- ...result,
799
- summary: buildMutationSummary(result),
800
- };
801
- }
802
-
803
- return {
804
- success: true,
805
- requestId: result.requestId,
806
- mateId: result.mateId,
807
- spaceId: result.spaceId,
808
- propId: result.propId,
809
- dailyLimit: result.dailyLimit,
810
- remaining: result.remaining,
811
- resetAtUtc: result.resetAtUtc,
812
- note: summarizeCreatedNote(result.note),
813
- summary: buildMutationSummary(result),
814
- };
908
+ service.createNotesBoardNote(propId.trim(), data.trim());
909
+ return { success: true };
815
910
  } catch (error) {
816
911
  return {
817
912
  success: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yahaha-studio/kichi-forwarder",
3
- "version": "0.0.1-alpha.26",
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",