@hyperspell/openclaw-hyperspell 0.12.0 → 0.13.1
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 +37 -1
- package/dist/client.js +341 -0
- package/dist/commands/setup.js +568 -0
- package/dist/commands/slash.js +159 -0
- package/dist/config.js +349 -0
- package/{graph/cron.ts → dist/graph/cron.js} +11 -15
- package/dist/graph/index.js +4 -0
- package/dist/graph/ops.js +221 -0
- package/dist/graph/state.js +68 -0
- package/dist/graph/tools.js +87 -0
- package/dist/hooks/auto-context.js +199 -0
- package/dist/hooks/auto-trace.js +143 -0
- package/dist/hooks/emotional-state.js +155 -0
- package/dist/hooks/memory-sync.js +54 -0
- package/dist/hooks/startup-orientation.js +236 -0
- package/dist/index.js +132 -0
- package/dist/lib/browser.js +29 -0
- package/dist/lib/sender.js +173 -0
- package/dist/lib/voice-id.js +22 -0
- package/dist/logger.js +32 -0
- package/dist/sync/markdown.js +151 -0
- package/dist/tools/remember.js +97 -0
- package/dist/tools/search.js +87 -0
- package/openclaw.plugin.json +25 -1
- package/package.json +7 -12
- package/client.ts +0 -566
- package/commands/setup.ts +0 -673
- package/commands/slash.ts +0 -198
- package/config.test.ts +0 -202
- package/config.ts +0 -497
- package/graph/index.ts +0 -5
- package/graph/ops.ts +0 -259
- package/graph/state.ts +0 -79
- package/graph/tools.ts +0 -117
- package/hooks/auto-context.ts +0 -272
- package/hooks/auto-trace.test.ts +0 -81
- package/hooks/auto-trace.ts +0 -197
- package/hooks/emotional-state.test.ts +0 -160
- package/hooks/emotional-state.ts +0 -179
- package/hooks/memory-sync.ts +0 -65
- package/index.ts +0 -152
- package/lib/browser.ts +0 -31
- package/lib/sender.test.ts +0 -234
- package/lib/sender.ts +0 -234
- package/lib/voice-id.ts +0 -39
- package/logger.ts +0 -41
- package/sync/markdown.ts +0 -186
- package/tools/remember.ts +0 -132
- package/tools/search.ts +0 -131
- package/types/openclaw.d.ts +0 -76
package/README.md
CHANGED
|
@@ -35,6 +35,9 @@ Add to your `openclaw.json`:
|
|
|
35
35
|
```json
|
|
36
36
|
{
|
|
37
37
|
"plugins": {
|
|
38
|
+
"slots": {
|
|
39
|
+
"memory": "openclaw-hyperspell"
|
|
40
|
+
},
|
|
38
41
|
"entries": {
|
|
39
42
|
"openclaw-hyperspell": {
|
|
40
43
|
"enabled": true,
|
|
@@ -43,8 +46,14 @@ Add to your `openclaw.json`:
|
|
|
43
46
|
"userId": "your-email",
|
|
44
47
|
"autoContext": true,
|
|
45
48
|
"syncMemories": true,
|
|
46
|
-
"sources": "vault,notion,slack"
|
|
49
|
+
"sources": "vault,notion,slack",
|
|
50
|
+
"dreaming": {
|
|
51
|
+
"enabled": true
|
|
52
|
+
}
|
|
47
53
|
}
|
|
54
|
+
},
|
|
55
|
+
"memory-core": {
|
|
56
|
+
"enabled": true
|
|
48
57
|
}
|
|
49
58
|
}
|
|
50
59
|
}
|
|
@@ -57,6 +66,28 @@ Set the environment variable in `~/.openclaw/.env`:
|
|
|
57
66
|
HYPERSPELL_API_KEY=hs_...
|
|
58
67
|
```
|
|
59
68
|
|
|
69
|
+
### Running alongside Dreaming
|
|
70
|
+
|
|
71
|
+
OpenClaw's memory slot (`plugins.slots.memory`) is exclusive, but the `memory-core` dreaming engine is an explicit exception: it can sidecar-load alongside the slot owner so Dreaming keeps running while Hyperspell handles remote search and storage.
|
|
72
|
+
|
|
73
|
+
To enable this, all four settings must be true at once:
|
|
74
|
+
|
|
75
|
+
| Setting | Value |
|
|
76
|
+
|---|---|
|
|
77
|
+
| `plugins.slots.memory` | `openclaw-hyperspell` |
|
|
78
|
+
| `plugins.entries.openclaw-hyperspell.enabled` | `true` |
|
|
79
|
+
| `plugins.entries.openclaw-hyperspell.config.dreaming.enabled` | `true` |
|
|
80
|
+
| `plugins.entries.memory-core.enabled` | `true` |
|
|
81
|
+
|
|
82
|
+
The `dreaming.enabled: true` flag on the Hyperspell plugin's config is what tells OpenClaw to load `memory-core` as a sidecar (see `resolveDreamingSidecarEngineId` in OpenClaw's plugin loader). Without it, `memory-core` is treated as a normal memory plugin and the slot policy disables it.
|
|
83
|
+
|
|
84
|
+
**Two distinct memory tracks:**
|
|
85
|
+
|
|
86
|
+
- **Hyperspell** — remote, cross-source (Slack, Notion, Drive, Gmail, vault, ...). Runs on every agent turn via `autoContext` and `autoTrace`. Owns the memory slot.
|
|
87
|
+
- **Dreaming** — local, consolidates daily notes and session transcripts into `workspace/MEMORY.md` on cron (light every 6h, deep nightly, REM weekly). Does not read Hyperspell's remote memories. Reads only local files the agent runner writes.
|
|
88
|
+
|
|
89
|
+
The two systems don't share state at runtime. They can be enabled independently.
|
|
90
|
+
|
|
60
91
|
## CLI Commands
|
|
61
92
|
|
|
62
93
|
### `openclaw openclaw-hyperspell setup`
|
|
@@ -82,6 +113,7 @@ Open the Hyperspell connect page to link your accounts (Notion, Slack, Google Dr
|
|
|
82
113
|
| `sources` | string | - | Comma-separated sources to search (e.g., `vault,notion,slack`) |
|
|
83
114
|
| `maxResults` | number | `10` | Maximum memories per context injection |
|
|
84
115
|
| `debug` | boolean | `false` | Enable verbose logging |
|
|
116
|
+
| `dreaming.enabled` | boolean | `false` | Allow `memory-core` to sidecar-load so Dreaming can consolidate local session transcripts into `workspace/MEMORY.md`. See [Running alongside Dreaming](#running-alongside-dreaming). |
|
|
85
117
|
|
|
86
118
|
## Slash Commands
|
|
87
119
|
|
|
@@ -212,6 +244,10 @@ Enable the graph tools by using `hyperspell_network_scan`, `hyperspell_network_w
|
|
|
212
244
|
- Enable `debug: true` to see what queries are being made
|
|
213
245
|
- Check that you have memories matching your conversation topics
|
|
214
246
|
|
|
247
|
+
### Enabling `memory-core` disabled Hyperspell
|
|
248
|
+
- This means `memory-core` was placed in `plugins.slots.memory`. The slot should stay on `openclaw-hyperspell`; `memory-core` rides alongside as a sidecar.
|
|
249
|
+
- Restore: set `plugins.slots.memory = "openclaw-hyperspell"`, set both plugins `enabled: true`, and add `dreaming: { enabled: true }` to the Hyperspell config. See [Running alongside Dreaming](#running-alongside-dreaming).
|
|
250
|
+
|
|
215
251
|
## Contributing
|
|
216
252
|
|
|
217
253
|
See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup and guidelines.
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,341 @@
|
|
|
1
|
+
import Hyperspell from "hyperspell";
|
|
2
|
+
import { normalizeScope } from "./config.js";
|
|
3
|
+
import { log } from "./logger.js";
|
|
4
|
+
const API_BASE_URL = "https://api.hyperspell.com";
|
|
5
|
+
export class HyperspellClient {
|
|
6
|
+
client;
|
|
7
|
+
config;
|
|
8
|
+
constructor(config) {
|
|
9
|
+
this.config = config;
|
|
10
|
+
this.client = new Hyperspell({
|
|
11
|
+
apiKey: config.apiKey,
|
|
12
|
+
userID: config.userId,
|
|
13
|
+
});
|
|
14
|
+
log.info(`client initialized${config.userId ? ` for user ${config.userId}` : ""}`);
|
|
15
|
+
}
|
|
16
|
+
rawHeaders() {
|
|
17
|
+
const headers = {
|
|
18
|
+
"Content-Type": "application/json",
|
|
19
|
+
Authorization: `Bearer ${this.config.apiKey}`,
|
|
20
|
+
};
|
|
21
|
+
if (this.config.userId) {
|
|
22
|
+
headers["X-As-User"] = this.config.userId;
|
|
23
|
+
}
|
|
24
|
+
return headers;
|
|
25
|
+
}
|
|
26
|
+
requestOptions(userId) {
|
|
27
|
+
if (!userId)
|
|
28
|
+
return undefined;
|
|
29
|
+
return { headers: { "X-As-User": userId } };
|
|
30
|
+
}
|
|
31
|
+
async search(query, options) {
|
|
32
|
+
const limit = options?.limit ?? this.config.maxResults;
|
|
33
|
+
const sources = options?.sources ??
|
|
34
|
+
(this.config.sources.length > 0 ? this.config.sources : undefined);
|
|
35
|
+
log.debugRequest("memories.search", {
|
|
36
|
+
query,
|
|
37
|
+
limit,
|
|
38
|
+
sources,
|
|
39
|
+
after: options?.after,
|
|
40
|
+
before: options?.before,
|
|
41
|
+
userId: options?.userId,
|
|
42
|
+
filter: options?.filter,
|
|
43
|
+
});
|
|
44
|
+
const response = await this.client.memories.search({
|
|
45
|
+
query,
|
|
46
|
+
sources,
|
|
47
|
+
options: {
|
|
48
|
+
max_results: limit,
|
|
49
|
+
...(options?.after ? { after: options.after } : {}),
|
|
50
|
+
...(options?.before ? { before: options.before } : {}),
|
|
51
|
+
...(options?.filter ? { filter: options.filter } : {}),
|
|
52
|
+
},
|
|
53
|
+
}, this.requestOptions(options?.userId));
|
|
54
|
+
const results = response.documents.map((doc) => {
|
|
55
|
+
const raw = doc;
|
|
56
|
+
return {
|
|
57
|
+
resourceId: doc.resource_id,
|
|
58
|
+
title: doc.title ?? null,
|
|
59
|
+
source: doc.source,
|
|
60
|
+
score: doc.score ?? null,
|
|
61
|
+
url: doc.metadata?.url ?? null,
|
|
62
|
+
createdAt: doc.metadata?.created_at ?? null,
|
|
63
|
+
highlights: (raw.highlights ?? []).map((h) => ({
|
|
64
|
+
id: h.id,
|
|
65
|
+
score: h.score,
|
|
66
|
+
text: h.text,
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
log.debugResponse("memories.search", { count: results.length });
|
|
71
|
+
return results;
|
|
72
|
+
}
|
|
73
|
+
async searchRaw(query, options) {
|
|
74
|
+
const limit = options?.limit ?? this.config.maxResults;
|
|
75
|
+
const sources = options?.sources ??
|
|
76
|
+
(this.config.sources.length > 0 ? this.config.sources : undefined);
|
|
77
|
+
log.debugRequest("memories.search (raw)", {
|
|
78
|
+
query,
|
|
79
|
+
limit,
|
|
80
|
+
sources,
|
|
81
|
+
after: options?.after,
|
|
82
|
+
before: options?.before,
|
|
83
|
+
userId: options?.userId,
|
|
84
|
+
filter: options?.filter,
|
|
85
|
+
});
|
|
86
|
+
const response = await this.client.memories.search({
|
|
87
|
+
query,
|
|
88
|
+
sources,
|
|
89
|
+
options: {
|
|
90
|
+
max_results: limit,
|
|
91
|
+
...(options?.after ? { after: options.after } : {}),
|
|
92
|
+
...(options?.before ? { before: options.before } : {}),
|
|
93
|
+
...(options?.filter ? { filter: options.filter } : {}),
|
|
94
|
+
},
|
|
95
|
+
}, this.requestOptions(options?.userId));
|
|
96
|
+
log.debugResponse("memories.search (raw)", {
|
|
97
|
+
count: response.documents.length,
|
|
98
|
+
});
|
|
99
|
+
return response;
|
|
100
|
+
}
|
|
101
|
+
async searchWithAnswer(query, options) {
|
|
102
|
+
const limit = options?.limit ?? this.config.maxResults;
|
|
103
|
+
const sources = options?.sources ??
|
|
104
|
+
(this.config.sources.length > 0 ? this.config.sources : undefined);
|
|
105
|
+
log.debugRequest("memories.search (with answer)", {
|
|
106
|
+
query,
|
|
107
|
+
limit,
|
|
108
|
+
sources,
|
|
109
|
+
userId: options?.userId,
|
|
110
|
+
filter: options?.filter,
|
|
111
|
+
});
|
|
112
|
+
const response = await this.client.memories.search({
|
|
113
|
+
query,
|
|
114
|
+
sources,
|
|
115
|
+
answer: true,
|
|
116
|
+
options: {
|
|
117
|
+
max_results: limit,
|
|
118
|
+
...(options?.filter ? { filter: options.filter } : {}),
|
|
119
|
+
},
|
|
120
|
+
}, this.requestOptions(options?.userId));
|
|
121
|
+
const documents = response.documents.map((doc) => ({
|
|
122
|
+
resourceId: doc.resource_id,
|
|
123
|
+
title: doc.title ?? null,
|
|
124
|
+
source: doc.source,
|
|
125
|
+
score: doc.score ?? null,
|
|
126
|
+
url: doc.metadata?.url ?? null,
|
|
127
|
+
createdAt: doc.metadata?.created_at ?? null,
|
|
128
|
+
highlights: [],
|
|
129
|
+
}));
|
|
130
|
+
log.debugResponse("memories.search (with answer)", {
|
|
131
|
+
count: documents.length,
|
|
132
|
+
hasAnswer: !!response.answer,
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
answer: response.answer ?? null,
|
|
136
|
+
documents,
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
async addMemory(text, options) {
|
|
140
|
+
log.debugRequest("memories.add", {
|
|
141
|
+
textLength: text.length,
|
|
142
|
+
title: options?.title,
|
|
143
|
+
resourceId: options?.resourceId,
|
|
144
|
+
collection: options?.collection,
|
|
145
|
+
date: options?.date,
|
|
146
|
+
userId: options?.userId,
|
|
147
|
+
scope: options?.scope,
|
|
148
|
+
});
|
|
149
|
+
const result = await this.client.memories.add({
|
|
150
|
+
text,
|
|
151
|
+
title: options?.title,
|
|
152
|
+
resource_id: options?.resourceId,
|
|
153
|
+
collection: options?.collection,
|
|
154
|
+
date: options?.date,
|
|
155
|
+
metadata: {
|
|
156
|
+
openclaw_source: "command",
|
|
157
|
+
...options?.metadata,
|
|
158
|
+
...(options?.userId ? { openclaw_user: options.userId } : {}),
|
|
159
|
+
...(options?.scope
|
|
160
|
+
? { openclaw_scope: normalizeScope(options.scope) }
|
|
161
|
+
: {}),
|
|
162
|
+
},
|
|
163
|
+
}, this.requestOptions(options?.userId));
|
|
164
|
+
log.debugResponse("memories.add", { resourceId: result.resource_id });
|
|
165
|
+
return { resourceId: result.resource_id };
|
|
166
|
+
}
|
|
167
|
+
async listIntegrations() {
|
|
168
|
+
log.debugRequest("integrations.list", {});
|
|
169
|
+
const response = await this.client.integrations.list();
|
|
170
|
+
const integrations = response.integrations.map((int) => ({
|
|
171
|
+
id: int.id,
|
|
172
|
+
name: int.name,
|
|
173
|
+
provider: int.provider,
|
|
174
|
+
icon: int.icon,
|
|
175
|
+
}));
|
|
176
|
+
log.debugResponse("integrations.list", { count: integrations.length });
|
|
177
|
+
return integrations;
|
|
178
|
+
}
|
|
179
|
+
async getConnectUrl(integrationId, options) {
|
|
180
|
+
log.debugRequest("integrations.connect", { integrationId });
|
|
181
|
+
const response = await this.client.integrations.connect(integrationId, undefined, this.requestOptions(options?.userId));
|
|
182
|
+
log.debugResponse("integrations.connect", { url: response.url });
|
|
183
|
+
return {
|
|
184
|
+
url: response.url,
|
|
185
|
+
expiresAt: response.expires_at,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
async *listMemories(options) {
|
|
189
|
+
log.debugRequest("memories.list", {
|
|
190
|
+
source: options?.source,
|
|
191
|
+
collection: options?.collection,
|
|
192
|
+
userId: options?.userId,
|
|
193
|
+
});
|
|
194
|
+
const params = {
|
|
195
|
+
size: options?.pageSize ?? 100,
|
|
196
|
+
};
|
|
197
|
+
if (options?.source)
|
|
198
|
+
params.source = options.source;
|
|
199
|
+
if (options?.collection)
|
|
200
|
+
params.collection = options.collection;
|
|
201
|
+
for await (const memory of this.client.memories.list(params, this.requestOptions(options?.userId))) {
|
|
202
|
+
yield {
|
|
203
|
+
resourceId: memory.resource_id,
|
|
204
|
+
source: memory.source,
|
|
205
|
+
title: memory.title ?? null,
|
|
206
|
+
metadata: (memory.metadata ?? {}),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
async getMemory(resourceId, source, options) {
|
|
211
|
+
log.debugRequest("memories.get", { resourceId, source });
|
|
212
|
+
const response = await this.client.memories.get(resourceId, { source }, this.requestOptions(options?.userId));
|
|
213
|
+
const raw = response;
|
|
214
|
+
log.debugResponse("memories.get", { resourceId, hasData: "data" in raw });
|
|
215
|
+
return raw;
|
|
216
|
+
}
|
|
217
|
+
async sendTrace(history, options) {
|
|
218
|
+
log.debugRequest("sessions.add", {
|
|
219
|
+
historyLength: history.length,
|
|
220
|
+
sessionId: options?.sessionId,
|
|
221
|
+
extract: options?.extract,
|
|
222
|
+
userId: options?.userId,
|
|
223
|
+
scope: options?.scope,
|
|
224
|
+
});
|
|
225
|
+
const result = await this.client.sessions.add({
|
|
226
|
+
history,
|
|
227
|
+
session_id: options?.sessionId,
|
|
228
|
+
title: options?.title,
|
|
229
|
+
format: "openclaw",
|
|
230
|
+
// Cast: SDK 0.35 typing accepts only ["procedure" | "memory"], but the
|
|
231
|
+
// backend's mood extractor (hyperspell/hyperspell#581) accepts "mood".
|
|
232
|
+
// Remove this cast once the OpenAPI spec is updated.
|
|
233
|
+
extract: (options?.extract ?? ["procedure"]),
|
|
234
|
+
metadata: {
|
|
235
|
+
openclaw_source: "agent_end",
|
|
236
|
+
...options?.metadata,
|
|
237
|
+
...(options?.userId ? { openclaw_user: options.userId } : {}),
|
|
238
|
+
...(options?.scope
|
|
239
|
+
? { openclaw_scope: normalizeScope(options.scope) }
|
|
240
|
+
: {}),
|
|
241
|
+
},
|
|
242
|
+
}, this.requestOptions(options?.userId));
|
|
243
|
+
log.debugResponse("sessions.add", {
|
|
244
|
+
resourceId: result.resource_id,
|
|
245
|
+
status: result.status,
|
|
246
|
+
});
|
|
247
|
+
return { resourceId: result.resource_id, status: result.status };
|
|
248
|
+
}
|
|
249
|
+
async listConnections(options) {
|
|
250
|
+
log.debugRequest("connections.list", { userId: options?.userId });
|
|
251
|
+
const response = await this.client.connections.list(this.requestOptions(options?.userId));
|
|
252
|
+
const connections = response.connections.map((conn) => ({
|
|
253
|
+
id: conn.id,
|
|
254
|
+
integrationId: conn.integration_id,
|
|
255
|
+
label: conn.label,
|
|
256
|
+
provider: conn.provider,
|
|
257
|
+
}));
|
|
258
|
+
log.debugResponse("connections.list", { count: connections.length });
|
|
259
|
+
return connections;
|
|
260
|
+
}
|
|
261
|
+
// -- Emotional State (raw fetch -- not in public SDK) -----------------------
|
|
262
|
+
async storeEmotionalState(conversation, options) {
|
|
263
|
+
log.debugRequest("emotional-state.store", {
|
|
264
|
+
conversationLength: conversation.length,
|
|
265
|
+
relationshipId: options?.relationshipId,
|
|
266
|
+
});
|
|
267
|
+
const body = { conversation };
|
|
268
|
+
if (options?.sessionId)
|
|
269
|
+
body.session_id = options.sessionId;
|
|
270
|
+
if (options?.relationshipId)
|
|
271
|
+
body.relationship_id = options.relationshipId;
|
|
272
|
+
if (options?.metadata)
|
|
273
|
+
body.metadata = options.metadata;
|
|
274
|
+
const res = await fetch(`${API_BASE_URL}/emotional-state`, {
|
|
275
|
+
method: "POST",
|
|
276
|
+
headers: this.rawHeaders(),
|
|
277
|
+
body: JSON.stringify(body),
|
|
278
|
+
});
|
|
279
|
+
if (!res.ok) {
|
|
280
|
+
const text = await res.text().catch(() => "");
|
|
281
|
+
throw new Error(`POST /emotional-state failed (${res.status}): ${text}`);
|
|
282
|
+
}
|
|
283
|
+
const data = await res.json();
|
|
284
|
+
const result = {
|
|
285
|
+
resourceId: data.resource_id,
|
|
286
|
+
summary: data.summary,
|
|
287
|
+
extractedAt: data.extracted_at,
|
|
288
|
+
sessionId: data.session_id ?? null,
|
|
289
|
+
relationshipId: data.relationship_id ?? null,
|
|
290
|
+
status: data.status,
|
|
291
|
+
};
|
|
292
|
+
log.debugResponse("emotional-state.store", { resourceId: result.resourceId });
|
|
293
|
+
return result;
|
|
294
|
+
}
|
|
295
|
+
async getEmotionalState(relationshipId) {
|
|
296
|
+
log.debugRequest("emotional-state.get", { relationshipId });
|
|
297
|
+
const url = new URL(`${API_BASE_URL}/emotional-state`);
|
|
298
|
+
if (relationshipId)
|
|
299
|
+
url.searchParams.set("relationship_id", relationshipId);
|
|
300
|
+
const res = await fetch(url.toString(), {
|
|
301
|
+
method: "GET",
|
|
302
|
+
headers: this.rawHeaders(),
|
|
303
|
+
});
|
|
304
|
+
if (!res.ok) {
|
|
305
|
+
const text = await res.text().catch(() => "");
|
|
306
|
+
throw new Error(`GET /emotional-state failed (${res.status}): ${text}`);
|
|
307
|
+
}
|
|
308
|
+
const data = await res.json();
|
|
309
|
+
if (data === null) {
|
|
310
|
+
log.debugResponse("emotional-state.get", { found: false });
|
|
311
|
+
return null;
|
|
312
|
+
}
|
|
313
|
+
const result = {
|
|
314
|
+
resourceId: data.resource_id,
|
|
315
|
+
summary: data.summary,
|
|
316
|
+
extractedAt: data.extracted_at,
|
|
317
|
+
sessionId: data.session_id ?? null,
|
|
318
|
+
relationshipId: data.relationship_id ?? null,
|
|
319
|
+
};
|
|
320
|
+
log.debugResponse("emotional-state.get", { found: true, resourceId: result.resourceId });
|
|
321
|
+
return result;
|
|
322
|
+
}
|
|
323
|
+
async deleteEmotionalState(relationshipId) {
|
|
324
|
+
log.debugRequest("emotional-state.delete", { relationshipId });
|
|
325
|
+
const url = new URL(`${API_BASE_URL}/emotional-state`);
|
|
326
|
+
if (relationshipId)
|
|
327
|
+
url.searchParams.set("relationship_id", relationshipId);
|
|
328
|
+
const res = await fetch(url.toString(), {
|
|
329
|
+
method: "DELETE",
|
|
330
|
+
headers: this.rawHeaders(),
|
|
331
|
+
});
|
|
332
|
+
if (!res.ok) {
|
|
333
|
+
const text = await res.text().catch(() => "");
|
|
334
|
+
throw new Error(`DELETE /emotional-state failed (${res.status}): ${text}`);
|
|
335
|
+
}
|
|
336
|
+
const data = await res.json();
|
|
337
|
+
const result = { deletedCount: data.deleted_count };
|
|
338
|
+
log.debugResponse("emotional-state.delete", result);
|
|
339
|
+
return result;
|
|
340
|
+
}
|
|
341
|
+
}
|