openfeelz 0.9.2 → 0.9.3

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/README.md CHANGED
@@ -7,7 +7,6 @@
7
7
  [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
8
8
  [![Node.js](https://img.shields.io/badge/node-%3E%3D22-brightgreen.svg)](https://nodejs.org/)
9
9
  [![TypeScript](https://img.shields.io/badge/TypeScript-strict-blue.svg)](https://www.typescriptlang.org/)
10
- [![OpenClaw Plugin](https://img.shields.io/badge/OpenClaw-plugin-purple.svg)](https://openclaw.com)
11
10
  [![npm](https://img.shields.io/npm/v/openfeelz.svg)](https://www.npmjs.com/package/openfeelz)
12
11
 
13
12
  An [OpenClaw](https://openclaw.com) plugin that gives AI agents a multidimensional emotional model with personality-influenced decay, rumination, and multi-agent awareness.
@@ -38,7 +37,7 @@ openclaw plugins install openfeelz
38
37
  openclaw plugins enable openfeelz
39
38
  ```
40
39
 
41
- Restart the gateway after installing. To pin a version: `openclaw plugins install openfeelz@0.9.2`. To install from a local clone (e.g. for development), run `npm run build` in the repo first, then `openclaw plugins install /path/to/openfeelz`.
40
+ Restart the gateway after installing. To pin a version: `openclaw plugins install openfeelz@0.9.3`. To install from a local clone (e.g. for development), run `npm run build` in the repo first, then `openclaw plugins install /path/to/openfeelz`.
42
41
 
43
42
  ## How It Works
44
43
 
@@ -179,11 +179,13 @@ async function classifyViaAnthropic(text, role, apiKey, model, fetchFn, timeoutM
179
179
  });
180
180
  if (!response.ok) {
181
181
  const body = await response.text().catch(() => "");
182
+ console.error("[openfeelz] Anthropic classification API error:", response.status, body.slice(0, 500));
182
183
  throw new Error(`Anthropic returned ${response.status}: ${body}`);
183
184
  }
184
185
  const data = (await response.json());
185
186
  const textBlock = data.content?.find((b) => b.type === "text");
186
187
  if (!textBlock?.text) {
188
+ console.error("[openfeelz] Anthropic classification returned no text block; content length:", data.content?.length ?? 0);
187
189
  throw new Error("Empty Anthropic response");
188
190
  }
189
191
  const parsed = parseClassifierResponse(textBlock.text);
@@ -208,15 +210,19 @@ async function classifyViaOpenAI(text, role, apiKey, baseUrl, model, fetchFn, ti
208
210
  ],
209
211
  temperature: 0.2,
210
212
  response_format: { type: "json_object" },
213
+ max_completion_tokens: 1000, // reasoning models (e.g. gpt-5-mini) need headroom
211
214
  }),
212
215
  signal: AbortSignal.timeout(timeoutMs),
213
216
  });
214
217
  if (!response.ok) {
218
+ const body = await response.text().catch(() => "");
219
+ console.error("[openfeelz] OpenAI classification API error:", response.status, body.slice(0, 500));
215
220
  throw new Error(`OpenAI returned ${response.status}`);
216
221
  }
217
222
  const data = (await response.json());
218
223
  const content = data.choices?.[0]?.message?.content;
219
224
  if (!content) {
225
+ console.error("[openfeelz] OpenAI classification returned no content; choices:", JSON.stringify(data.choices?.length ?? 0));
220
226
  throw new Error("Empty OpenAI response");
221
227
  }
222
228
  const parsed = parseClassifierResponse(content);
@@ -18,7 +18,7 @@ interface AgentEndEvent {
18
18
  success: boolean;
19
19
  messages: Array<{
20
20
  role: string;
21
- content: string;
21
+ content: unknown;
22
22
  }>;
23
23
  userKey?: string;
24
24
  agentId?: string;
@@ -7,6 +7,7 @@
7
7
  import { loadOtherAgentStatesFromConfig } from "../state/multi-agent.js";
8
8
  import { classifyEmotion } from "../classify/classifier.js";
9
9
  import { formatEmotionBlock } from "../format/prompt-formatter.js";
10
+ import { extractMessageText } from "../utils/message-content.js";
10
11
  // ---------------------------------------------------------------------------
11
12
  // Bootstrap Hook (before_agent_start)
12
13
  // ---------------------------------------------------------------------------
@@ -99,17 +100,23 @@ export function createAgentEndHook(getManager, config, fetchFn) {
99
100
  fetchFn,
100
101
  };
101
102
  if (userMsg) {
102
- const result = await classifyEmotion(userMsg.content, "user", classifyOpts);
103
- if (result.label !== "neutral" || result.confidence > 0) {
104
- state = manager.updateUserEmotion(state, userKey, result);
105
- // Also apply as stimulus to the dimensional model
106
- state = manager.applyStimulus(state, result.label, result.intensity, result.reason);
103
+ const text = extractMessageText(userMsg.content);
104
+ if (text) {
105
+ const result = await classifyEmotion(text, "user", classifyOpts);
106
+ if (result.label !== "neutral" || result.confidence > 0) {
107
+ state = manager.updateUserEmotion(state, userKey, result);
108
+ // Also apply as stimulus to the dimensional model
109
+ state = manager.applyStimulus(state, result.label, result.intensity, result.reason);
110
+ }
107
111
  }
108
112
  }
109
113
  if (assistantMsg) {
110
- const result = await classifyEmotion(assistantMsg.content, "assistant", classifyOpts);
111
- if (result.label !== "neutral" || result.confidence > 0) {
112
- state = manager.updateAgentEmotion(state, agentId, result);
114
+ const text = extractMessageText(assistantMsg.content);
115
+ if (text) {
116
+ const result = await classifyEmotion(text, "assistant", classifyOpts);
117
+ if (result.label !== "neutral" || result.confidence > 0) {
118
+ state = manager.updateAgentEmotion(state, agentId, result);
119
+ }
113
120
  }
114
121
  }
115
122
  await manager.saveState(state);
@@ -124,8 +131,9 @@ export function createAgentEndHook(getManager, config, fetchFn) {
124
131
  // ---------------------------------------------------------------------------
125
132
  function findLast(messages, role) {
126
133
  for (let i = messages.length - 1; i >= 0; i--) {
127
- if (messages[i].role === role && messages[i].content.trim()) {
128
- return messages[i];
134
+ const msg = messages[i];
135
+ if (msg.role === role && extractMessageText(msg.content) !== "") {
136
+ return msg;
129
137
  }
130
138
  }
131
139
  return undefined;
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Extract plain text from OpenClaw message content for use in hooks/tools.
3
+ *
4
+ * OpenClaw can send content as:
5
+ * - string (simple messages)
6
+ * - array of content blocks (e.g. [{ type: "text", text: "..." }])
7
+ * - other (object/undefined) in some code paths
8
+ *
9
+ * Semantics match OpenClaw core's extractSessionText (memory/session-files.ts)
10
+ * so plugin behavior is consistent with core message handling.
11
+ */
12
+ export declare function extractMessageText(content: unknown): string;
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Extract plain text from OpenClaw message content for use in hooks/tools.
3
+ *
4
+ * OpenClaw can send content as:
5
+ * - string (simple messages)
6
+ * - array of content blocks (e.g. [{ type: "text", text: "..." }])
7
+ * - other (object/undefined) in some code paths
8
+ *
9
+ * Semantics match OpenClaw core's extractSessionText (memory/session-files.ts)
10
+ * so plugin behavior is consistent with core message handling.
11
+ */
12
+ export function extractMessageText(content) {
13
+ if (typeof content === "string") {
14
+ const trimmed = content.trim();
15
+ return trimmed;
16
+ }
17
+ if (!Array.isArray(content)) {
18
+ return "";
19
+ }
20
+ const parts = [];
21
+ for (const block of content) {
22
+ if (!block || typeof block !== "object") {
23
+ continue;
24
+ }
25
+ const record = block;
26
+ if (record.type !== "text" || typeof record.text !== "string") {
27
+ continue;
28
+ }
29
+ const trimmed = String(record.text).trim();
30
+ if (trimmed) {
31
+ parts.push(trimmed);
32
+ }
33
+ }
34
+ return parts.join(" ");
35
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "openfeelz",
3
- "version": "0.9.2",
3
+ "version": "0.9.3",
4
4
  "description": "PAD + Ekman + OCEAN emotional model plugin for OpenClaw agents",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",