@hyperspell/openclaw-hyperspell 0.4.2 → 0.7.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 +127 -3
- package/client.ts +311 -202
- package/commands/setup.ts +190 -55
- package/commands/wine.ts +164 -0
- package/config.ts +244 -162
- package/graph/cron.ts +364 -0
- package/graph/index.ts +5 -0
- package/graph/ops.ts +253 -0
- package/graph/state.ts +79 -0
- package/graph/tools.ts +117 -0
- package/hooks/auto-context.ts +17 -11
- package/hooks/auto-trace.ts +127 -0
- package/index.ts +123 -95
- package/lib/browser.ts +10 -6
- package/lib/run-script.ts +32 -0
- package/openclaw.plugin.json +110 -74
- package/package.json +6 -2
- package/sommeliagent/references/cross-domain-mappings.md +63 -0
- package/sommeliagent/scripts/auth.py +222 -0
- package/sommeliagent/scripts/history.py +72 -0
- package/sommeliagent/scripts/rate.py +76 -0
- package/sommeliagent/scripts/recommend.py +777 -0
- package/sommeliagent/scripts/test_recommend.py +459 -0
- package/sommeliagent/scripts/wine_db.py +3224 -0
- package/tools/remember.ts +6 -2
- package/tools/search.ts +9 -3
- package/tools/sommelier.ts +254 -0
package/graph/tools.ts
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { Type } from "@sinclair/typebox"
|
|
2
|
+
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
3
|
+
import type { HyperspellClient } from "../client.ts"
|
|
4
|
+
import type { HyperspellConfig } from "../config.ts"
|
|
5
|
+
import { getWorkspaceDir } from "../config.ts"
|
|
6
|
+
import { log } from "../logger.ts"
|
|
7
|
+
import { NetworkStateManager } from "./state.ts"
|
|
8
|
+
import { scanMemories, formatScanResults, writeEntity, completeMemories } from "./ops.ts"
|
|
9
|
+
import type { EntityType, SourceMemories } from "./ops.ts"
|
|
10
|
+
|
|
11
|
+
const ENTITY_TYPES = ["people", "projects", "organizations", "topics"] as const
|
|
12
|
+
|
|
13
|
+
export function registerNetworkTools(
|
|
14
|
+
api: OpenClawPluginApi,
|
|
15
|
+
client: HyperspellClient,
|
|
16
|
+
cfg: HyperspellConfig,
|
|
17
|
+
): void {
|
|
18
|
+
const workspaceDir = getWorkspaceDir()
|
|
19
|
+
const stateManager = new NetworkStateManager(workspaceDir)
|
|
20
|
+
|
|
21
|
+
api.registerTool(
|
|
22
|
+
{
|
|
23
|
+
name: "hyperspell_network_scan",
|
|
24
|
+
label: "Memory Network Scan",
|
|
25
|
+
description:
|
|
26
|
+
"Scan Hyperspell memories and return a batch of unprocessed ones with their content summaries. Use this to find new memories that need entity extraction.",
|
|
27
|
+
parameters: Type.Object({
|
|
28
|
+
batchSize: Type.Optional(
|
|
29
|
+
Type.Number({ description: "Max memories to return (default: 20)" }),
|
|
30
|
+
),
|
|
31
|
+
}),
|
|
32
|
+
async execute(_toolCallId: string, params: { batchSize?: number }) {
|
|
33
|
+
const batchSize = params.batchSize ?? cfg.knowledgeGraph.batchSize
|
|
34
|
+
try {
|
|
35
|
+
const memories = await scanMemories(client, stateManager, batchSize)
|
|
36
|
+
const text = formatScanResults(memories, stateManager.getProcessedCount(), stateManager.getLastScanAt())
|
|
37
|
+
return {
|
|
38
|
+
content: [{ type: "text" as const, text }],
|
|
39
|
+
details: { count: memories.length, memories },
|
|
40
|
+
}
|
|
41
|
+
} catch (err) {
|
|
42
|
+
log.error("network scan failed", err)
|
|
43
|
+
return {
|
|
44
|
+
content: [{ type: "text" as const, text: `Scan failed: ${err instanceof Error ? err.message : String(err)}` }],
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
{ name: "hyperspell_network_scan" },
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
api.registerTool(
|
|
53
|
+
{
|
|
54
|
+
name: "hyperspell_network_write",
|
|
55
|
+
label: "Memory Network Write",
|
|
56
|
+
description:
|
|
57
|
+
"Write or update an entity file in the memory network. Creates markdown files in memory/people/, memory/projects/, memory/organizations/, or memory/topics/.",
|
|
58
|
+
parameters: Type.Object({
|
|
59
|
+
type: Type.Union(ENTITY_TYPES.map((t) => Type.Literal(t)), {
|
|
60
|
+
description: "Entity type: people, projects, organizations, or topics",
|
|
61
|
+
}),
|
|
62
|
+
slug: Type.String({ description: "URL-safe identifier (lowercase with hyphens, e.g. 'alice-chen')" }),
|
|
63
|
+
name: Type.String({ description: "Display name of the entity" }),
|
|
64
|
+
description: Type.String({ description: "Description of the entity" }),
|
|
65
|
+
relationships: Type.Optional(
|
|
66
|
+
Type.Array(Type.String(), {
|
|
67
|
+
description: "Relationships in format 'relationship:type/slug', e.g. 'works-at:organizations/hyperspell'",
|
|
68
|
+
}),
|
|
69
|
+
),
|
|
70
|
+
sourceMemories: Type.Optional(
|
|
71
|
+
Type.Record(Type.String(), Type.Array(Type.String()), {
|
|
72
|
+
description: "Source memories by provider, e.g. { slack: ['C073WR69EPM'], google_mail: ['19bbe68026553623'] }",
|
|
73
|
+
}),
|
|
74
|
+
),
|
|
75
|
+
email: Type.Optional(Type.String({ description: "Email address (for people)" })),
|
|
76
|
+
phone: Type.Optional(Type.String({ description: "Phone number (for people)" })),
|
|
77
|
+
domain: Type.Optional(Type.String({ description: "Domain/homepage (for organizations)" })),
|
|
78
|
+
}),
|
|
79
|
+
async execute(
|
|
80
|
+
_toolCallId: string,
|
|
81
|
+
params: { type: EntityType; slug: string; name: string; description: string; relationships?: string[]; sourceMemories?: SourceMemories; email?: string; phone?: string; domain?: string },
|
|
82
|
+
) {
|
|
83
|
+
try {
|
|
84
|
+
const result = writeEntity(workspaceDir, params)
|
|
85
|
+
return { content: [{ type: "text" as const, text: `Wrote ${result} (${params.name})` }] }
|
|
86
|
+
} catch (err) {
|
|
87
|
+
log.error(`network write failed: ${params.type}/${params.slug}`, err)
|
|
88
|
+
return { content: [{ type: "text" as const, text: `Write failed: ${err instanceof Error ? err.message : String(err)}` }] }
|
|
89
|
+
}
|
|
90
|
+
},
|
|
91
|
+
},
|
|
92
|
+
{ name: "hyperspell_network_write" },
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
api.registerTool(
|
|
96
|
+
{
|
|
97
|
+
name: "hyperspell_network_complete",
|
|
98
|
+
label: "Memory Network Complete",
|
|
99
|
+
description: "Mark a batch of memory IDs as processed so they won't appear in future scans.",
|
|
100
|
+
parameters: Type.Object({
|
|
101
|
+
memoryIds: Type.Array(Type.String(), { description: "List of resource_ids to mark as processed" }),
|
|
102
|
+
}),
|
|
103
|
+
async execute(_toolCallId: string, params: { memoryIds: string[] }) {
|
|
104
|
+
try {
|
|
105
|
+
const { newCount, totalCount } = completeMemories(stateManager, params.memoryIds)
|
|
106
|
+
return {
|
|
107
|
+
content: [{ type: "text" as const, text: `Marked ${newCount} new memories as processed (${totalCount} total). Last scan: ${stateManager.getLastScanAt()}` }],
|
|
108
|
+
}
|
|
109
|
+
} catch (err) {
|
|
110
|
+
log.error("network complete failed", err)
|
|
111
|
+
return { content: [{ type: "text" as const, text: `Complete failed: ${err instanceof Error ? err.message : String(err)}` }] }
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
},
|
|
115
|
+
{ name: "hyperspell_network_complete" },
|
|
116
|
+
)
|
|
117
|
+
}
|
package/hooks/auto-context.ts
CHANGED
|
@@ -26,25 +26,31 @@ function formatRelativeTime(isoTimestamp: string): string {
|
|
|
26
26
|
}
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
function formatContext(results: SearchResult[], maxResults: number): string | null {
|
|
30
|
-
const
|
|
29
|
+
function formatContext(results: SearchResult[], maxResults: number, threshold: number): string | null {
|
|
30
|
+
const sections: string[] = []
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
for (const r of results.slice(0, maxResults)) {
|
|
33
|
+
if ((r.score ?? 0) < threshold) continue
|
|
34
|
+
|
|
35
|
+
const aboveThreshold = r.highlights.filter((h) => h.score >= threshold)
|
|
36
|
+
if (aboveThreshold.length === 0) continue
|
|
33
37
|
|
|
34
|
-
const lines = limited.map((r) => {
|
|
35
38
|
const title = r.title ?? `[${r.source}]`
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
39
|
+
const bullets = aboveThreshold
|
|
40
|
+
.map((h) => `- ${h.text.replace(/\n/g, " ")} [${Math.round(h.score * 100)}%]`)
|
|
41
|
+
.join("\n")
|
|
42
|
+
|
|
43
|
+
sections.push(`### ${title} (resource_id: ${r.resourceId}, source: ${r.source})\n\n${bullets}`)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (sections.length === 0) return null
|
|
41
47
|
|
|
42
48
|
const intro =
|
|
43
49
|
"The following is context from the user's connected sources. Reference it only when relevant to the conversation."
|
|
44
50
|
const disclaimer =
|
|
45
51
|
"Use this context naturally when relevant — including indirect connections — but don't force it into every response or make assumptions beyond what's stated."
|
|
46
52
|
|
|
47
|
-
return `<hyperspell-context>\n${intro}\n\n
|
|
53
|
+
return `<hyperspell-context>\n${intro}\n\n${sections.join("\n\n")}\n\n${disclaimer}\n</hyperspell-context>`
|
|
48
54
|
}
|
|
49
55
|
|
|
50
56
|
export function buildAutoContextHandler(
|
|
@@ -59,7 +65,7 @@ export function buildAutoContextHandler(
|
|
|
59
65
|
|
|
60
66
|
try {
|
|
61
67
|
const results = await client.search(prompt, { limit: cfg.maxResults })
|
|
62
|
-
const context = formatContext(results, cfg.maxResults)
|
|
68
|
+
const context = formatContext(results, cfg.maxResults, cfg.relevanceThreshold)
|
|
63
69
|
|
|
64
70
|
if (!context) {
|
|
65
71
|
log.debug("auto-context: no relevant memories found")
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import type { HyperspellClient } from "../client.ts";
|
|
2
|
+
import type { HyperspellConfig } from "../config.ts";
|
|
3
|
+
import { log } from "../logger.ts";
|
|
4
|
+
|
|
5
|
+
type Message = { role?: string; content?: string | unknown };
|
|
6
|
+
|
|
7
|
+
const MIN_MESSAGES = 3;
|
|
8
|
+
const MIN_CONVERSATION_LENGTH = 100;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Convert event.messages into OpenClaw JSONL format for the trace API.
|
|
12
|
+
*/
|
|
13
|
+
function messagesToJSONL(messages: unknown[], sessionId: string): string {
|
|
14
|
+
const lines: string[] = [];
|
|
15
|
+
|
|
16
|
+
// Session header
|
|
17
|
+
lines.push(
|
|
18
|
+
JSON.stringify({
|
|
19
|
+
type: "session",
|
|
20
|
+
version: 3,
|
|
21
|
+
id: sessionId,
|
|
22
|
+
timestamp: new Date().toISOString(),
|
|
23
|
+
}),
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
// Message entries
|
|
27
|
+
for (const raw of messages as Message[]) {
|
|
28
|
+
if (!raw.role || !raw.content) continue;
|
|
29
|
+
|
|
30
|
+
const content =
|
|
31
|
+
typeof raw.content === "string"
|
|
32
|
+
? [{ type: "text", text: raw.content }]
|
|
33
|
+
: Array.isArray(raw.content)
|
|
34
|
+
? raw.content
|
|
35
|
+
: [{ type: "text", text: JSON.stringify(raw.content) }];
|
|
36
|
+
|
|
37
|
+
const id = crypto.randomUUID().slice(0, 8);
|
|
38
|
+
|
|
39
|
+
if (raw.role === "tool" || raw.role === "toolResult") {
|
|
40
|
+
// Tool results use a different structure
|
|
41
|
+
lines.push(
|
|
42
|
+
JSON.stringify({
|
|
43
|
+
type: "message",
|
|
44
|
+
id,
|
|
45
|
+
timestamp: new Date().toISOString(),
|
|
46
|
+
message: {
|
|
47
|
+
role: "toolResult",
|
|
48
|
+
toolCallId: (raw as Record<string, unknown>).toolCallId ?? id,
|
|
49
|
+
toolName: (raw as Record<string, unknown>).toolName ?? "unknown",
|
|
50
|
+
content,
|
|
51
|
+
isError: (raw as Record<string, unknown>).isError ?? false,
|
|
52
|
+
},
|
|
53
|
+
}),
|
|
54
|
+
);
|
|
55
|
+
} else {
|
|
56
|
+
lines.push(
|
|
57
|
+
JSON.stringify({
|
|
58
|
+
type: "message",
|
|
59
|
+
id,
|
|
60
|
+
timestamp: new Date().toISOString(),
|
|
61
|
+
message: { role: raw.role, content },
|
|
62
|
+
}),
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return lines.join("\n");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Extract and store conversation trace at session end.
|
|
72
|
+
* Runs on `agent_end` — fire-and-forget.
|
|
73
|
+
*/
|
|
74
|
+
export function buildAutoTraceHandler(
|
|
75
|
+
client: HyperspellClient,
|
|
76
|
+
cfg: HyperspellConfig,
|
|
77
|
+
) {
|
|
78
|
+
return async (event: Record<string, unknown>) => {
|
|
79
|
+
if (event.success === false) {
|
|
80
|
+
log.debug("auto-trace: skipping — agent ended with error");
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const messages = event.messages as unknown[] | undefined;
|
|
85
|
+
if (!messages || messages.length < MIN_MESSAGES) {
|
|
86
|
+
log.debug(
|
|
87
|
+
`auto-trace: skipping — too few messages (${messages?.length ?? 0})`,
|
|
88
|
+
);
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
// Quick content length check
|
|
93
|
+
const estimate = (messages as Message[])
|
|
94
|
+
.filter((m) => m.content)
|
|
95
|
+
.reduce((acc, m) => acc + String(m.content).length, 0);
|
|
96
|
+
if (estimate < MIN_CONVERSATION_LENGTH) {
|
|
97
|
+
log.debug(
|
|
98
|
+
`auto-trace: skipping — conversation too short (${estimate} chars)`,
|
|
99
|
+
);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const sessionId = (event.sessionId as string) ?? crypto.randomUUID();
|
|
104
|
+
const history = messagesToJSONL(messages, sessionId);
|
|
105
|
+
|
|
106
|
+
// Title from first user message
|
|
107
|
+
const firstUser = (messages as Message[]).find((m) => m.role === "user");
|
|
108
|
+
const title = firstUser?.content
|
|
109
|
+
? String(firstUser.content).slice(0, 80).replace(/\n/g, " ")
|
|
110
|
+
: undefined;
|
|
111
|
+
|
|
112
|
+
try {
|
|
113
|
+
const result = await client.sendTrace(history, {
|
|
114
|
+
sessionId,
|
|
115
|
+
title,
|
|
116
|
+
extract: cfg.autoTrace.extract,
|
|
117
|
+
metadata: cfg.autoTrace.metadata,
|
|
118
|
+
});
|
|
119
|
+
log.info(
|
|
120
|
+
`auto-trace: sent ${result.resourceId} (${messages.length} messages)`,
|
|
121
|
+
);
|
|
122
|
+
} catch (err) {
|
|
123
|
+
// Fire-and-forget — never break the session
|
|
124
|
+
log.error("auto-trace failed", err);
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
}
|
package/index.ts
CHANGED
|
@@ -1,107 +1,135 @@
|
|
|
1
1
|
import type { OpenClawPluginApi } from "openclaw/plugin-sdk"
|
|
2
2
|
import { HyperspellClient } from "./client.ts"
|
|
3
3
|
import { registerCommands } from "./commands/slash.ts"
|
|
4
|
+
import { registerWineCommands } from "./commands/wine.ts"
|
|
4
5
|
import { registerCliCommands } from "./commands/setup.ts"
|
|
5
6
|
import { parseConfig, hyperspellConfigSchema, getWorkspaceDir } from "./config.ts"
|
|
6
7
|
import { buildAutoContextHandler } from "./hooks/auto-context.ts"
|
|
8
|
+
import { buildAutoTraceHandler } from "./hooks/auto-trace.ts"
|
|
7
9
|
import { buildFileSyncHandler, syncMemoriesOnStartup } from "./hooks/memory-sync.ts"
|
|
8
10
|
import { initLogger } from "./logger.ts"
|
|
9
11
|
import { registerRememberTool } from "./tools/remember.ts"
|
|
10
12
|
import { registerSearchTool } from "./tools/search.ts"
|
|
13
|
+
import { registerSommelierTool, registerSommelierRateTool } from "./tools/sommelier.ts"
|
|
14
|
+
import { registerNetworkTools } from "./graph/index.ts"
|
|
11
15
|
|
|
12
16
|
export default {
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
17
|
+
id: "openclaw-hyperspell",
|
|
18
|
+
name: "Hyperspell",
|
|
19
|
+
description:
|
|
20
|
+
"Hyperspell gives your Molty context and memory from all your existing data",
|
|
21
|
+
kind: "memory" as const,
|
|
22
|
+
configSchema: hyperspellConfigSchema,
|
|
23
|
+
|
|
24
|
+
register(api: OpenClawPluginApi) {
|
|
25
|
+
// Register CLI commands (openclaw openclaw-hyperspell setup|status|connect)
|
|
26
|
+
api.registerCli(
|
|
27
|
+
(ctx) => {
|
|
28
|
+
registerCliCommands(ctx.program, api.pluginConfig);
|
|
29
|
+
},
|
|
30
|
+
{ commands: ["openclaw-hyperspell"] },
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
// Check if configured
|
|
34
|
+
const rawConfig = api.pluginConfig as Record<string, unknown> | undefined;
|
|
35
|
+
const hasConfig = rawConfig?.apiKey || process.env.HYPERSPELL_API_KEY;
|
|
36
|
+
|
|
37
|
+
// SommeliAgent works independently of Hyperspell config — it just needs uv + Spotify
|
|
38
|
+
registerSommelierTool(api)
|
|
39
|
+
registerSommelierRateTool(api)
|
|
40
|
+
registerWineCommands(api)
|
|
41
|
+
|
|
42
|
+
if (!hasConfig) {
|
|
43
|
+
api.logger.info(
|
|
44
|
+
"hyperspell: not configured - run 'openclaw openclaw-hyperspell setup'",
|
|
45
|
+
)
|
|
46
|
+
// Still register slash commands so they show up, but they'll return an error
|
|
47
|
+
api.registerCommand({
|
|
48
|
+
name: "getcontext",
|
|
49
|
+
description: "Search your memories for relevant context",
|
|
50
|
+
acceptsArgs: true,
|
|
51
|
+
requireAuth: false,
|
|
52
|
+
handler: async () => {
|
|
53
|
+
return {
|
|
54
|
+
text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first.",
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
api.registerCommand({
|
|
59
|
+
name: "remember",
|
|
60
|
+
description: "Save something to memory",
|
|
61
|
+
acceptsArgs: true,
|
|
62
|
+
requireAuth: false,
|
|
63
|
+
handler: async () => {
|
|
64
|
+
return {
|
|
65
|
+
text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first.",
|
|
66
|
+
}
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
api.registerCommand({
|
|
70
|
+
name: "sync",
|
|
71
|
+
description: "Sync memory/*.md files with Hyperspell",
|
|
72
|
+
acceptsArgs: false,
|
|
73
|
+
requireAuth: false,
|
|
74
|
+
handler: async () => {
|
|
75
|
+
return {
|
|
76
|
+
text: "Hyperspell not configured. Run 'openclaw openclaw-hyperspell setup' first.",
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
})
|
|
80
|
+
return
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const cfg = parseConfig(api.pluginConfig);
|
|
84
|
+
|
|
85
|
+
initLogger(api.logger, cfg.debug);
|
|
86
|
+
|
|
87
|
+
const client = new HyperspellClient(cfg);
|
|
88
|
+
|
|
89
|
+
// Register AI tools
|
|
90
|
+
registerSearchTool(api, client, cfg);
|
|
91
|
+
registerRememberTool(api, client, cfg);
|
|
92
|
+
|
|
93
|
+
// Register auto-context hook
|
|
94
|
+
if (cfg.autoContext) {
|
|
95
|
+
const autoContextHandler = buildAutoContextHandler(client, cfg);
|
|
96
|
+
api.on("before_agent_start", autoContextHandler);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Register auto-trace hook (send conversations to Hyperspell on session end)
|
|
100
|
+
if (cfg.autoTrace.enabled) {
|
|
101
|
+
api.on("agent_end", buildAutoTraceHandler(client, cfg));
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Register memory sync hook
|
|
105
|
+
if (cfg.syncMemories) {
|
|
106
|
+
const fileSyncHandler = buildFileSyncHandler(client, cfg);
|
|
107
|
+
api.on("file_changed", fileSyncHandler);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Register memory network tools
|
|
111
|
+
if (cfg.knowledgeGraph.enabled) {
|
|
112
|
+
registerNetworkTools(api, client, cfg);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Register slash commands
|
|
116
|
+
registerCommands(api, client, cfg);
|
|
117
|
+
|
|
118
|
+
// Register service for lifecycle management
|
|
119
|
+
api.registerService({
|
|
120
|
+
id: "openclaw-hyperspell",
|
|
121
|
+
start: async () => {
|
|
122
|
+
api.logger.info("hyperspell: connected");
|
|
123
|
+
|
|
124
|
+
// Sync memories on startup if enabled
|
|
125
|
+
if (cfg.syncMemories) {
|
|
126
|
+
const workspaceDir = getWorkspaceDir();
|
|
127
|
+
await syncMemoriesOnStartup(client, workspaceDir);
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
stop: () => {
|
|
131
|
+
api.logger.info("hyperspell: stopped");
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
},
|
|
135
|
+
};
|
package/lib/browser.ts
CHANGED
|
@@ -1,22 +1,26 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { execFile } from "node:child_process"
|
|
2
2
|
import { platform } from "node:os"
|
|
3
3
|
|
|
4
4
|
export function openInBrowser(url: string): Promise<void> {
|
|
5
5
|
return new Promise((resolve, reject) => {
|
|
6
|
-
let
|
|
6
|
+
let file: string
|
|
7
|
+
let args: string[]
|
|
7
8
|
|
|
8
9
|
switch (platform()) {
|
|
9
10
|
case "darwin":
|
|
10
|
-
|
|
11
|
+
file = "open"
|
|
12
|
+
args = [url]
|
|
11
13
|
break
|
|
12
14
|
case "win32":
|
|
13
|
-
|
|
15
|
+
file = "cmd"
|
|
16
|
+
args = ["/c", "start", "", url]
|
|
14
17
|
break
|
|
15
18
|
default:
|
|
16
|
-
|
|
19
|
+
file = "xdg-open"
|
|
20
|
+
args = [url]
|
|
17
21
|
}
|
|
18
22
|
|
|
19
|
-
|
|
23
|
+
execFile(file, args, (error) => {
|
|
20
24
|
if (error) {
|
|
21
25
|
reject(error)
|
|
22
26
|
} else {
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { execFile } from "node:child_process"
|
|
2
|
+
import { dirname, join } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url))
|
|
6
|
+
export const SCRIPTS_DIR = join(__dirname, "..", "sommeliagent", "scripts")
|
|
7
|
+
|
|
8
|
+
const ALLOWED_SCRIPTS = new Set(["recommend.py", "rate.py", "auth.py", "history.py"])
|
|
9
|
+
|
|
10
|
+
export function runScript(
|
|
11
|
+
scriptName: string,
|
|
12
|
+
args: string[],
|
|
13
|
+
timeout: number = 30_000,
|
|
14
|
+
): Promise<{ stdout: string; stderr: string }> {
|
|
15
|
+
if (!ALLOWED_SCRIPTS.has(scriptName)) {
|
|
16
|
+
return Promise.reject(new Error(`Unknown script: ${scriptName}`))
|
|
17
|
+
}
|
|
18
|
+
return new Promise((resolve, reject) => {
|
|
19
|
+
execFile(
|
|
20
|
+
"uv",
|
|
21
|
+
["run", join(SCRIPTS_DIR, scriptName), ...args],
|
|
22
|
+
{ timeout, maxBuffer: 1024 * 512 },
|
|
23
|
+
(error, stdout, stderr) => {
|
|
24
|
+
if (error) {
|
|
25
|
+
reject(new Error(`${scriptName} failed: ${stderr || error.message}`))
|
|
26
|
+
} else {
|
|
27
|
+
resolve({ stdout, stderr })
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
)
|
|
31
|
+
})
|
|
32
|
+
}
|