@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
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { getWorkspaceDir } from "../config.js";
|
|
2
|
+
import { buildScopeFilter, getCanReadScopes, getDefaultWriteScope, resolveRole, resolveUser, routeWrite, } from "../lib/sender.js";
|
|
3
|
+
import { log } from "../logger.js";
|
|
4
|
+
import { syncAllMemoryFiles } from "../sync/markdown.js";
|
|
5
|
+
/**
|
|
6
|
+
* Strip a `#scope-name` prefix from free text. Returns the scope and the
|
|
7
|
+
* remainder. Used by /remember and /getcontext to let users narrow or route
|
|
8
|
+
* via a single keystroke.
|
|
9
|
+
*/
|
|
10
|
+
function parseScopePrefix(text) {
|
|
11
|
+
const m = text.match(/^#(\S+)\s+(.*)$/);
|
|
12
|
+
if (!m)
|
|
13
|
+
return { rest: text };
|
|
14
|
+
return { scope: m[1], rest: m[2] };
|
|
15
|
+
}
|
|
16
|
+
function truncate(text, maxLength) {
|
|
17
|
+
if (text.length <= maxLength)
|
|
18
|
+
return text;
|
|
19
|
+
return `${text.slice(0, maxLength)}…`;
|
|
20
|
+
}
|
|
21
|
+
function formatScore(score) {
|
|
22
|
+
if (score === null)
|
|
23
|
+
return "";
|
|
24
|
+
return ` (${Math.round(score * 100)}%)`;
|
|
25
|
+
}
|
|
26
|
+
export function registerCommands(api, client, cfg) {
|
|
27
|
+
// /getcontext <query> - Search memories and show summaries
|
|
28
|
+
api.registerCommand({
|
|
29
|
+
name: "getcontext",
|
|
30
|
+
description: "Search your memories for relevant context",
|
|
31
|
+
acceptsArgs: true,
|
|
32
|
+
requireAuth: true,
|
|
33
|
+
handler: async (ctx) => {
|
|
34
|
+
const rawArgs = ctx.args?.trim();
|
|
35
|
+
if (!rawArgs) {
|
|
36
|
+
return { text: "Usage: /getcontext [#scope] <search query>" };
|
|
37
|
+
}
|
|
38
|
+
const { scope: requestedScope, rest: query } = parseScopePrefix(rawArgs);
|
|
39
|
+
const resolved = resolveUser(ctx, cfg);
|
|
40
|
+
const userId = resolved?.userId;
|
|
41
|
+
// Build scope filter: intersect requested scope (if any) with caller's canRead.
|
|
42
|
+
let filter;
|
|
43
|
+
if (cfg.multiUser?.scoping) {
|
|
44
|
+
const canRead = getCanReadScopes(resolved, cfg);
|
|
45
|
+
const allowed = requestedScope
|
|
46
|
+
? canRead.includes("*") || canRead.includes(requestedScope)
|
|
47
|
+
? [requestedScope]
|
|
48
|
+
: []
|
|
49
|
+
: canRead;
|
|
50
|
+
filter = buildScopeFilter(allowed, resolved?.userId ?? "");
|
|
51
|
+
}
|
|
52
|
+
log.debug(`/getcontext command: "${query}" userId=${userId} scope=${requestedScope ?? "any"}`);
|
|
53
|
+
try {
|
|
54
|
+
const results = await client.search(query, { limit: 5, userId, filter });
|
|
55
|
+
if (results.length === 0) {
|
|
56
|
+
return { text: `No memories found for: "${query}"` };
|
|
57
|
+
}
|
|
58
|
+
const lines = results.map((r, i) => {
|
|
59
|
+
const title = r.title ? truncate(r.title, 60) : `[${r.source}]`;
|
|
60
|
+
const score = formatScore(r.score);
|
|
61
|
+
return `${i + 1}. ${title}${score}`;
|
|
62
|
+
});
|
|
63
|
+
return {
|
|
64
|
+
text: `Found ${results.length} memories:\n\n${lines.join("\n")}`,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
catch (err) {
|
|
68
|
+
log.error("/getcontext failed", err);
|
|
69
|
+
return { text: "Failed to search memories. Check logs for details." };
|
|
70
|
+
}
|
|
71
|
+
},
|
|
72
|
+
});
|
|
73
|
+
// /remember <text> - Add a new memory
|
|
74
|
+
api.registerCommand({
|
|
75
|
+
name: "remember",
|
|
76
|
+
description: "Save something to memory",
|
|
77
|
+
acceptsArgs: true,
|
|
78
|
+
requireAuth: true,
|
|
79
|
+
handler: async (ctx) => {
|
|
80
|
+
const rawArgs = ctx.args?.trim();
|
|
81
|
+
if (!rawArgs) {
|
|
82
|
+
return { text: "Usage: /remember [#scope] <text to remember>" };
|
|
83
|
+
}
|
|
84
|
+
const { scope: requestedScope, rest: text } = parseScopePrefix(rawArgs);
|
|
85
|
+
const resolved = resolveUser(ctx, cfg);
|
|
86
|
+
const scopingEnabled = !!cfg.multiUser?.scoping;
|
|
87
|
+
const availableScopes = cfg.multiUser?.scoping?.scopes ?? [];
|
|
88
|
+
// Scope resolution: prefix > role default > global default > "private"
|
|
89
|
+
const scope = scopingEnabled
|
|
90
|
+
? (requestedScope ?? getDefaultWriteScope(resolved, cfg))
|
|
91
|
+
: "private";
|
|
92
|
+
// Validate scope is in declared vocabulary
|
|
93
|
+
if (scopingEnabled &&
|
|
94
|
+
requestedScope &&
|
|
95
|
+
availableScopes.length > 0 &&
|
|
96
|
+
!availableScopes.includes(scope)) {
|
|
97
|
+
return {
|
|
98
|
+
text: `Unknown scope "${scope}". Available: ${availableScopes.join(", ")}.`,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
// canWriteScopes enforcement
|
|
102
|
+
if (scopingEnabled) {
|
|
103
|
+
const role = resolveRole(resolved, cfg);
|
|
104
|
+
if (role?.canWriteScopes && !role.canWriteScopes.includes(scope)) {
|
|
105
|
+
return { text: `You cannot write to scope "${scope}".` };
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
const { userId, collection } = scopingEnabled
|
|
109
|
+
? routeWrite(resolved, scope, cfg)
|
|
110
|
+
: { userId: resolved?.userId, collection: undefined };
|
|
111
|
+
log.debug(`/remember command: "${truncate(text, 50)}" userId=${userId} scope=${scope}`);
|
|
112
|
+
try {
|
|
113
|
+
await client.addMemory(text, {
|
|
114
|
+
metadata: { source: "openclaw_command" },
|
|
115
|
+
collection,
|
|
116
|
+
userId,
|
|
117
|
+
scope: scopingEnabled ? scope : undefined,
|
|
118
|
+
});
|
|
119
|
+
const preview = truncate(text, 60);
|
|
120
|
+
const scopeHint = scopingEnabled ? ` [${scope}]` : "";
|
|
121
|
+
return { text: `Remembered${scopeHint}: "${preview}"` };
|
|
122
|
+
}
|
|
123
|
+
catch (err) {
|
|
124
|
+
log.error("/remember failed", err);
|
|
125
|
+
return { text: "Failed to save memory. Check logs for details." };
|
|
126
|
+
}
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
// /sync - Manually sync memory files
|
|
130
|
+
api.registerCommand({
|
|
131
|
+
name: "sync",
|
|
132
|
+
description: "Sync memory/*.md files with Hyperspell",
|
|
133
|
+
acceptsArgs: false,
|
|
134
|
+
requireAuth: true,
|
|
135
|
+
handler: async () => {
|
|
136
|
+
log.debug("/sync command");
|
|
137
|
+
try {
|
|
138
|
+
const workspaceDir = getWorkspaceDir();
|
|
139
|
+
const result = await syncAllMemoryFiles(client, workspaceDir, {
|
|
140
|
+
userId: cfg.multiUser?.sharedUserId,
|
|
141
|
+
});
|
|
142
|
+
if (result.synced === 0 && result.failed === 0) {
|
|
143
|
+
return { text: "No memory files found in memory/ directory." };
|
|
144
|
+
}
|
|
145
|
+
if (result.failed > 0) {
|
|
146
|
+
const errors = result.errors.map((e) => ` • ${e}`).join("\n");
|
|
147
|
+
return {
|
|
148
|
+
text: `Synced ${result.synced} files, ${result.failed} failed:\n${errors}`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
return { text: `Synced ${result.synced} memory file(s) to Hyperspell.` };
|
|
152
|
+
}
|
|
153
|
+
catch (err) {
|
|
154
|
+
log.error("/sync failed", err);
|
|
155
|
+
return { text: "Failed to sync memory files. Check logs for details." };
|
|
156
|
+
}
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
}
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,349 @@
|
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as os from "node:os";
|
|
3
|
+
import * as path from "node:path";
|
|
4
|
+
/**
|
|
5
|
+
* Convert user-facing scope names (which may contain hyphens) to SDK-safe
|
|
6
|
+
* metadata values (alphanumeric + underscore only). Must be applied at every
|
|
7
|
+
* boundary where scopes cross into Hyperspell metadata — writes and reads —
|
|
8
|
+
* or filters will silently miss.
|
|
9
|
+
*/
|
|
10
|
+
export function normalizeScope(scope) {
|
|
11
|
+
return scope.replace(/[^a-zA-Z0-9_]/g, "_");
|
|
12
|
+
}
|
|
13
|
+
const ALLOWED_KEYS = [
|
|
14
|
+
"apiKey",
|
|
15
|
+
"userId",
|
|
16
|
+
"autoContext",
|
|
17
|
+
"autoTrace",
|
|
18
|
+
"emotionalContext",
|
|
19
|
+
"relationshipId",
|
|
20
|
+
"startupOrientation",
|
|
21
|
+
"syncMemories",
|
|
22
|
+
"sources",
|
|
23
|
+
"maxResults",
|
|
24
|
+
"relevanceThreshold",
|
|
25
|
+
"debug",
|
|
26
|
+
"knowledgeGraph",
|
|
27
|
+
"multiUser",
|
|
28
|
+
"dreaming",
|
|
29
|
+
];
|
|
30
|
+
const VALID_SOURCES = [
|
|
31
|
+
"reddit",
|
|
32
|
+
"notion",
|
|
33
|
+
"slack",
|
|
34
|
+
"google_calendar",
|
|
35
|
+
"google_mail",
|
|
36
|
+
"box",
|
|
37
|
+
"google_drive",
|
|
38
|
+
"vault",
|
|
39
|
+
"web_crawler",
|
|
40
|
+
"dropbox",
|
|
41
|
+
"github",
|
|
42
|
+
"trace",
|
|
43
|
+
"microsoft_teams",
|
|
44
|
+
];
|
|
45
|
+
function assertAllowedKeys(value, allowed, label) {
|
|
46
|
+
const unknown = Object.keys(value).filter((k) => !allowed.includes(k));
|
|
47
|
+
if (unknown.length > 0) {
|
|
48
|
+
throw new Error(`${label} has unknown keys: ${unknown.join(", ")}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
function resolveEnvVars(value) {
|
|
52
|
+
return value.replace(/\$\{([^}]+)\}/g, (_, envVar) => {
|
|
53
|
+
const envValue = process.env[envVar];
|
|
54
|
+
if (!envValue) {
|
|
55
|
+
throw new Error(`Environment variable ${envVar} is not set`);
|
|
56
|
+
}
|
|
57
|
+
return envValue;
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
function parseSources(raw) {
|
|
61
|
+
if (!raw) {
|
|
62
|
+
return [];
|
|
63
|
+
}
|
|
64
|
+
// Handle array input
|
|
65
|
+
if (Array.isArray(raw)) {
|
|
66
|
+
const sources = raw
|
|
67
|
+
.map((s) => String(s).trim().toLowerCase())
|
|
68
|
+
.filter((s) => s.length > 0);
|
|
69
|
+
for (const source of sources) {
|
|
70
|
+
if (!VALID_SOURCES.includes(source)) {
|
|
71
|
+
throw new Error(`Invalid source "${source}". Valid sources: ${VALID_SOURCES.join(", ")}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return sources;
|
|
75
|
+
}
|
|
76
|
+
// Handle string input (comma-separated)
|
|
77
|
+
if (typeof raw === "string" && raw.trim() === "") {
|
|
78
|
+
return [];
|
|
79
|
+
}
|
|
80
|
+
const sources = raw
|
|
81
|
+
.split(",")
|
|
82
|
+
.map((s) => s.trim().toLowerCase())
|
|
83
|
+
.filter((s) => s.length > 0);
|
|
84
|
+
for (const source of sources) {
|
|
85
|
+
if (!VALID_SOURCES.includes(source)) {
|
|
86
|
+
throw new Error(`Invalid source "${source}". Valid sources: ${VALID_SOURCES.join(", ")}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return sources;
|
|
90
|
+
}
|
|
91
|
+
function parseScoping(raw) {
|
|
92
|
+
if (!raw || typeof raw !== "object")
|
|
93
|
+
return undefined;
|
|
94
|
+
const sc = raw;
|
|
95
|
+
if (sc.enabled !== true)
|
|
96
|
+
return undefined;
|
|
97
|
+
const scopes = Array.isArray(sc.scopes)
|
|
98
|
+
? sc.scopes.filter((s) => typeof s === "string")
|
|
99
|
+
: [];
|
|
100
|
+
if (scopes.length === 0) {
|
|
101
|
+
throw new Error("scoping.scopes must be a non-empty array of scope names");
|
|
102
|
+
}
|
|
103
|
+
const defaultScope = typeof sc.defaultScope === "string" ? sc.defaultScope : "private";
|
|
104
|
+
if (!scopes.includes(defaultScope)) {
|
|
105
|
+
throw new Error(`scoping.defaultScope "${defaultScope}" must be one of scopes: ${scopes.join(", ")}`);
|
|
106
|
+
}
|
|
107
|
+
const rolesRaw = sc.roles && typeof sc.roles === "object"
|
|
108
|
+
? sc.roles
|
|
109
|
+
: {};
|
|
110
|
+
const roles = {};
|
|
111
|
+
for (const [roleName, rRaw] of Object.entries(rolesRaw)) {
|
|
112
|
+
if (!rRaw || typeof rRaw !== "object")
|
|
113
|
+
continue;
|
|
114
|
+
const r = rRaw;
|
|
115
|
+
const canRead = Array.isArray(r.canRead)
|
|
116
|
+
? r.canRead.filter((s) => typeof s === "string")
|
|
117
|
+
: [];
|
|
118
|
+
for (const s of canRead) {
|
|
119
|
+
if (s === "*" || s === "self")
|
|
120
|
+
continue;
|
|
121
|
+
if (!scopes.includes(s)) {
|
|
122
|
+
throw new Error(`scoping.roles.${roleName}.canRead contains unknown scope "${s}"`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
const defaultWriteScope = typeof r.defaultWriteScope === "string"
|
|
126
|
+
? r.defaultWriteScope
|
|
127
|
+
: defaultScope;
|
|
128
|
+
if (!scopes.includes(defaultWriteScope)) {
|
|
129
|
+
throw new Error(`scoping.roles.${roleName}.defaultWriteScope "${defaultWriteScope}" must be one of scopes: ${scopes.join(", ")}`);
|
|
130
|
+
}
|
|
131
|
+
let canWriteScopes;
|
|
132
|
+
if (Array.isArray(r.canWriteScopes)) {
|
|
133
|
+
canWriteScopes = r.canWriteScopes.filter((s) => typeof s === "string");
|
|
134
|
+
for (const s of canWriteScopes) {
|
|
135
|
+
if (!scopes.includes(s)) {
|
|
136
|
+
throw new Error(`scoping.roles.${roleName}.canWriteScopes contains unknown scope "${s}"`);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
roles[roleName] = { canRead, defaultWriteScope, canWriteScopes };
|
|
141
|
+
}
|
|
142
|
+
const usersRaw = sc.users && typeof sc.users === "object"
|
|
143
|
+
? sc.users
|
|
144
|
+
: {};
|
|
145
|
+
const users = {};
|
|
146
|
+
for (const [uid, uRaw] of Object.entries(usersRaw)) {
|
|
147
|
+
if (!uRaw || typeof uRaw !== "object")
|
|
148
|
+
continue;
|
|
149
|
+
const u = uRaw;
|
|
150
|
+
if (typeof u.role !== "string")
|
|
151
|
+
continue;
|
|
152
|
+
if (!roles[u.role]) {
|
|
153
|
+
throw new Error(`scoping.users.${uid}.role "${u.role}" does not key into scoping.roles`);
|
|
154
|
+
}
|
|
155
|
+
users[uid] = { role: u.role };
|
|
156
|
+
}
|
|
157
|
+
const collectionsRaw = sc.collections && typeof sc.collections === "object"
|
|
158
|
+
? sc.collections
|
|
159
|
+
: undefined;
|
|
160
|
+
let collections;
|
|
161
|
+
if (collectionsRaw) {
|
|
162
|
+
collections = {};
|
|
163
|
+
for (const [scopeName, coll] of Object.entries(collectionsRaw)) {
|
|
164
|
+
if (typeof coll === "string" && scopes.includes(scopeName)) {
|
|
165
|
+
collections[scopeName] = coll;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
let voiceId;
|
|
170
|
+
if (sc.voiceId && typeof sc.voiceId === "object") {
|
|
171
|
+
const v = sc.voiceId;
|
|
172
|
+
voiceId = {
|
|
173
|
+
enabled: v.enabled === true,
|
|
174
|
+
adapter: typeof v.adapter === "string" ? v.adapter : undefined,
|
|
175
|
+
confidenceThreshold: typeof v.confidenceThreshold === "number"
|
|
176
|
+
? v.confidenceThreshold
|
|
177
|
+
: undefined,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
enabled: true,
|
|
182
|
+
defaultScope,
|
|
183
|
+
scopes,
|
|
184
|
+
roles,
|
|
185
|
+
users,
|
|
186
|
+
collections,
|
|
187
|
+
voiceId,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
function parseMultiUser(raw) {
|
|
191
|
+
if (!raw || typeof raw !== "object")
|
|
192
|
+
return undefined;
|
|
193
|
+
const mu = raw;
|
|
194
|
+
const senderMap = {};
|
|
195
|
+
const rawMap = mu.senderMap;
|
|
196
|
+
if (rawMap && typeof rawMap === "object") {
|
|
197
|
+
for (const [handle, profile] of Object.entries(rawMap)) {
|
|
198
|
+
if (profile && typeof profile === "object") {
|
|
199
|
+
const p = profile;
|
|
200
|
+
if (typeof p.userId === "string" && typeof p.name === "string") {
|
|
201
|
+
senderMap[handle] = {
|
|
202
|
+
userId: p.userId,
|
|
203
|
+
name: p.name,
|
|
204
|
+
context: typeof p.context === "string" ? p.context : undefined,
|
|
205
|
+
role: typeof p.role === "string" ? p.role : undefined,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
if (Object.keys(senderMap).length === 0)
|
|
212
|
+
return undefined;
|
|
213
|
+
return {
|
|
214
|
+
senderMap,
|
|
215
|
+
sharedUserId: typeof mu.sharedUserId === "string" ? mu.sharedUserId : "shared",
|
|
216
|
+
includeSharedInSearch: typeof mu.includeSharedInSearch === "boolean"
|
|
217
|
+
? mu.includeSharedInSearch
|
|
218
|
+
: true,
|
|
219
|
+
scoping: parseScoping(mu.scoping),
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function parseConfig(raw) {
|
|
223
|
+
const cfg = raw && typeof raw === "object" && !Array.isArray(raw)
|
|
224
|
+
? raw
|
|
225
|
+
: {};
|
|
226
|
+
if (Object.keys(cfg).length > 0) {
|
|
227
|
+
assertAllowedKeys(cfg, ALLOWED_KEYS, "hyperspell config");
|
|
228
|
+
}
|
|
229
|
+
const apiKey = typeof cfg.apiKey === "string" && cfg.apiKey.length > 0
|
|
230
|
+
? resolveEnvVars(cfg.apiKey)
|
|
231
|
+
: process.env.HYPERSPELL_API_KEY;
|
|
232
|
+
if (!apiKey) {
|
|
233
|
+
throw new Error("hyperspell: apiKey is required (set in plugin config or HYPERSPELL_API_KEY env var)");
|
|
234
|
+
}
|
|
235
|
+
const kgRaw = (cfg.knowledgeGraph ?? {});
|
|
236
|
+
const atRaw = (cfg.autoTrace ?? {});
|
|
237
|
+
const soRaw = (cfg.startupOrientation ?? {});
|
|
238
|
+
return {
|
|
239
|
+
apiKey,
|
|
240
|
+
userId: cfg.userId,
|
|
241
|
+
autoContext: cfg.autoContext ?? true,
|
|
242
|
+
autoTrace: {
|
|
243
|
+
enabled: atRaw.enabled ?? false,
|
|
244
|
+
extract: atRaw.extract ?? [
|
|
245
|
+
"procedure",
|
|
246
|
+
],
|
|
247
|
+
metadata: atRaw.metadata,
|
|
248
|
+
},
|
|
249
|
+
emotionalContext: cfg.emotionalContext ?? false,
|
|
250
|
+
relationshipId: cfg.relationshipId,
|
|
251
|
+
startupOrientation: {
|
|
252
|
+
enabled: soRaw.enabled ?? false,
|
|
253
|
+
recentDays: soRaw.recentDays ?? 7,
|
|
254
|
+
recentLimit: soRaw.recentLimit ?? 5,
|
|
255
|
+
loopsLimit: soRaw.loopsLimit ?? 3,
|
|
256
|
+
loopsQuery: soRaw.loopsQuery ??
|
|
257
|
+
"open tasks pending questions unfinished promised need to follow up",
|
|
258
|
+
},
|
|
259
|
+
syncMemories: cfg.syncMemories ?? false,
|
|
260
|
+
sources: parseSources(cfg.sources),
|
|
261
|
+
maxResults: cfg.maxResults ?? 10,
|
|
262
|
+
relevanceThreshold: cfg.relevanceThreshold ?? 0.6,
|
|
263
|
+
debug: cfg.debug ?? false,
|
|
264
|
+
knowledgeGraph: {
|
|
265
|
+
enabled: kgRaw.enabled ?? false,
|
|
266
|
+
scanIntervalMinutes: kgRaw.scanIntervalMinutes ?? 60,
|
|
267
|
+
batchSize: kgRaw.batchSize ?? 20,
|
|
268
|
+
},
|
|
269
|
+
multiUser: parseMultiUser(cfg.multiUser),
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
export const hyperspellConfigSchema = {
|
|
273
|
+
parse: parseConfig,
|
|
274
|
+
};
|
|
275
|
+
/**
|
|
276
|
+
* Resolve OpenClaw state directory (matches OpenClaw's own logic).
|
|
277
|
+
*/
|
|
278
|
+
export function resolveStateDir() {
|
|
279
|
+
const override = process.env.OPENCLAW_STATE_DIR?.trim() ||
|
|
280
|
+
process.env.CLAWDBOT_STATE_DIR?.trim();
|
|
281
|
+
if (override) {
|
|
282
|
+
return override.startsWith("~")
|
|
283
|
+
? override.replace(/^~(?=$|[\\/])/, os.homedir())
|
|
284
|
+
: path.resolve(override);
|
|
285
|
+
}
|
|
286
|
+
return path.join(os.homedir(), ".openclaw");
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Resolve OpenClaw config file path (matches OpenClaw's own logic).
|
|
290
|
+
*/
|
|
291
|
+
export function resolveConfigPath() {
|
|
292
|
+
const override = process.env.OPENCLAW_CONFIG_PATH?.trim() ||
|
|
293
|
+
process.env.CLAWDBOT_CONFIG_PATH?.trim();
|
|
294
|
+
if (override) {
|
|
295
|
+
return override.startsWith("~")
|
|
296
|
+
? override.replace(/^~(?=$|[\\/])/, os.homedir())
|
|
297
|
+
: path.resolve(override);
|
|
298
|
+
}
|
|
299
|
+
return path.join(resolveStateDir(), "openclaw.json");
|
|
300
|
+
}
|
|
301
|
+
/**
|
|
302
|
+
* Get the workspace directory from OpenClaw config
|
|
303
|
+
*/
|
|
304
|
+
export function getWorkspaceDir() {
|
|
305
|
+
// Resolve config path
|
|
306
|
+
const override = process.env.OPENCLAW_CONFIG_PATH?.trim() ||
|
|
307
|
+
process.env.CLAWDBOT_CONFIG_PATH?.trim();
|
|
308
|
+
let configPath;
|
|
309
|
+
if (override) {
|
|
310
|
+
configPath = override.startsWith("~")
|
|
311
|
+
? override.replace(/^~(?=$|[\\/])/, os.homedir())
|
|
312
|
+
: path.resolve(override);
|
|
313
|
+
}
|
|
314
|
+
else {
|
|
315
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() ||
|
|
316
|
+
process.env.CLAWDBOT_STATE_DIR?.trim();
|
|
317
|
+
const resolvedStateDir = stateDir
|
|
318
|
+
? stateDir.startsWith("~")
|
|
319
|
+
? stateDir.replace(/^~(?=$|[\\/])/, os.homedir())
|
|
320
|
+
: path.resolve(stateDir)
|
|
321
|
+
: path.join(os.homedir(), ".openclaw");
|
|
322
|
+
configPath = path.join(resolvedStateDir, "openclaw.json");
|
|
323
|
+
}
|
|
324
|
+
// Read workspace from config
|
|
325
|
+
if (fs.existsSync(configPath)) {
|
|
326
|
+
try {
|
|
327
|
+
const content = fs.readFileSync(configPath, "utf-8");
|
|
328
|
+
const config = JSON.parse(content);
|
|
329
|
+
const workspace = config?.agents?.defaults?.workspace;
|
|
330
|
+
if (workspace) {
|
|
331
|
+
return workspace.startsWith("~")
|
|
332
|
+
? workspace.replace(/^~(?=$|[\\/])/, os.homedir())
|
|
333
|
+
: workspace;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
catch (_e) {
|
|
337
|
+
// Fall back to default
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
// Default workspace
|
|
341
|
+
const stateDir = process.env.OPENCLAW_STATE_DIR?.trim() ||
|
|
342
|
+
process.env.CLAWDBOT_STATE_DIR?.trim();
|
|
343
|
+
const resolvedStateDir = stateDir
|
|
344
|
+
? stateDir.startsWith("~")
|
|
345
|
+
? stateDir.replace(/^~(?=$|[\\/])/, os.homedir())
|
|
346
|
+
: path.resolve(stateDir)
|
|
347
|
+
: path.join(os.homedir(), ".openclaw");
|
|
348
|
+
return path.join(resolvedStateDir, "workspace");
|
|
349
|
+
}
|
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
export const CRON_JOB_NAME = "Hyperspell Memory Network"
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
return `You are a memory network builder. Your job is to scan memories and extract structured entities into markdown files.
|
|
1
|
+
export const CRON_JOB_NAME = "Hyperspell Memory Network";
|
|
2
|
+
export function buildExtractionPrompt(workspaceDir) {
|
|
3
|
+
const memoryDir = `${workspaceDir}/memory`;
|
|
4
|
+
return `You are a memory network builder. Your job is to scan memories and extract structured entities into markdown files.
|
|
7
5
|
|
|
8
6
|
## How it works
|
|
9
7
|
|
|
@@ -350,15 +348,13 @@ Retrieval-Augmented Generation — technique for grounding LLM responses in retr
|
|
|
350
348
|
- **Cross-reference**: Use relationships to connect people to organizations, projects to topics, etc.
|
|
351
349
|
- **Contact info is critical**: For people, always capture email addresses from sender/participant data. For organizations, derive their domain from email addresses (e.g. alice@hyperspell.com → hyperspell.com).
|
|
352
350
|
- **source_memories format**: JSON object with source provider as key and array of resource_ids as value.
|
|
353
|
-
- **graph_entity: true**: Always include this — it prevents the scan from re-processing entity files that get synced back to Hyperspell
|
|
351
|
+
- **graph_entity: true**: Always include this — it prevents the scan from re-processing entity files that get synced back to Hyperspell.`;
|
|
354
352
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
return `openclaw cron add --name '${CRON_JOB_NAME}' --every ${interval} --session isolated --message '${escaped}'`
|
|
353
|
+
export function getCronSetupCommand(workspaceDir, interval = "1h") {
|
|
354
|
+
const prompt = buildExtractionPrompt(workspaceDir);
|
|
355
|
+
const escaped = prompt.replace(/'/g, "'\\''");
|
|
356
|
+
return `openclaw cron add --name '${CRON_JOB_NAME}' --every ${interval} --session isolated --message '${escaped}'`;
|
|
360
357
|
}
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
return `openclaw cron remove --name '${CRON_JOB_NAME}'`
|
|
358
|
+
export function getCronRemoveCommand() {
|
|
359
|
+
return `openclaw cron remove --name '${CRON_JOB_NAME}'`;
|
|
364
360
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { NetworkStateManager } from "./state.js";
|
|
2
|
+
export { registerNetworkTools } from "./tools.js";
|
|
3
|
+
export { scanMemories, formatScanResults, writeEntity, completeMemories, slugify } from "./ops.js";
|
|
4
|
+
export { buildExtractionPrompt, getCronSetupCommand, getCronRemoveCommand, CRON_JOB_NAME } from "./cron.js";
|