@keyoku/openclaw 1.0.0 → 1.1.0
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/bin/init.js +15 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +58 -0
- package/dist/cli.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +21 -1
- package/dist/context.js.map +1 -1
- package/dist/heartbeat-setup.d.ts +8 -1
- package/dist/heartbeat-setup.d.ts.map +1 -1
- package/dist/heartbeat-setup.js +58 -15
- package/dist/heartbeat-setup.js.map +1 -1
- package/dist/hooks.d.ts.map +1 -1
- package/dist/hooks.js +76 -50
- package/dist/hooks.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +21 -0
- package/dist/init.d.ts.map +1 -0
- package/dist/init.js +522 -0
- package/dist/init.js.map +1 -0
- package/dist/migrate-vector-store.d.ts +52 -0
- package/dist/migrate-vector-store.d.ts.map +1 -0
- package/dist/migrate-vector-store.js +158 -0
- package/dist/migrate-vector-store.js.map +1 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +27 -3
- package/dist/service.js.map +1 -1
- package/package.json +13 -4
- package/skills/keyoku-memory/SKILL.md +67 -0
- package/src/capture.ts +0 -116
- package/src/cli.ts +0 -95
- package/src/config.ts +0 -43
- package/src/context.ts +0 -164
- package/src/heartbeat-setup.ts +0 -53
- package/src/hooks.ts +0 -175
- package/src/incremental-capture.ts +0 -88
- package/src/index.ts +0 -68
- package/src/migration.ts +0 -241
- package/src/service.ts +0 -145
- package/src/tools.ts +0 -239
- package/src/types.ts +0 -40
- package/test/capture.test.ts +0 -139
- package/test/context.test.ts +0 -273
- package/test/hooks.test.ts +0 -137
- package/test/tools.test.ts +0 -174
- package/tsconfig.json +0 -8
package/src/migration.ts
DELETED
|
@@ -1,241 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Migration utility — imports OpenClaw's file-based memories into Keyoku.
|
|
3
|
-
*
|
|
4
|
-
* Reads MEMORY.md and memory/*.md files, chunks them by heading sections,
|
|
5
|
-
* deduplicates against existing Keyoku memories, and stores each chunk.
|
|
6
|
-
*
|
|
7
|
-
* Usage: `openclaw memory import --dir /path/to/workspace`
|
|
8
|
-
*/
|
|
9
|
-
|
|
10
|
-
import { readFileSync, readdirSync, existsSync, statSync } from 'fs';
|
|
11
|
-
import { join, basename } from 'path';
|
|
12
|
-
import type { KeyokuClient } from '@keyoku/memory';
|
|
13
|
-
|
|
14
|
-
export interface ImportResult {
|
|
15
|
-
imported: number;
|
|
16
|
-
skipped: number;
|
|
17
|
-
errors: number;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
interface MemoryChunk {
|
|
21
|
-
content: string;
|
|
22
|
-
source: string; // original filename
|
|
23
|
-
section?: string; // heading text
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
/**
|
|
27
|
-
* Split markdown content by ## or ### headings.
|
|
28
|
-
* Each heading section becomes one chunk.
|
|
29
|
-
* If no headings, split by --- separators or paragraphs.
|
|
30
|
-
*/
|
|
31
|
-
function chunkByHeadings(content: string, maxChunkChars = 1000): MemoryChunk[] {
|
|
32
|
-
const chunks: MemoryChunk[] = [];
|
|
33
|
-
|
|
34
|
-
// Try splitting by headings first
|
|
35
|
-
const headingPattern = /^#{2,3}\s+(.+)$/gm;
|
|
36
|
-
const headings: { index: number; title: string }[] = [];
|
|
37
|
-
let match: RegExpExecArray | null;
|
|
38
|
-
|
|
39
|
-
while ((match = headingPattern.exec(content)) !== null) {
|
|
40
|
-
headings.push({ index: match.index, title: match[1].trim() });
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
if (headings.length > 0) {
|
|
44
|
-
for (let i = 0; i < headings.length; i++) {
|
|
45
|
-
const start = headings[i].index;
|
|
46
|
-
const end = i + 1 < headings.length ? headings[i + 1].index : content.length;
|
|
47
|
-
const sectionText = content.slice(start, end).trim();
|
|
48
|
-
|
|
49
|
-
if (sectionText.length < 10) continue;
|
|
50
|
-
|
|
51
|
-
// If section is too long, split by paragraphs
|
|
52
|
-
if (sectionText.length > maxChunkChars) {
|
|
53
|
-
const paragraphs = splitByParagraphs(sectionText, maxChunkChars);
|
|
54
|
-
for (const p of paragraphs) {
|
|
55
|
-
chunks.push({ content: p, source: '', section: headings[i].title });
|
|
56
|
-
}
|
|
57
|
-
} else {
|
|
58
|
-
chunks.push({ content: sectionText, source: '', section: headings[i].title });
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
// Content before the first heading
|
|
63
|
-
const preamble = content.slice(0, headings[0].index).trim();
|
|
64
|
-
if (preamble.length >= 10) {
|
|
65
|
-
const paragraphs = splitByParagraphs(preamble, maxChunkChars);
|
|
66
|
-
for (const p of paragraphs) {
|
|
67
|
-
chunks.push({ content: p, source: '' });
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
} else {
|
|
71
|
-
// No headings — try --- separators
|
|
72
|
-
const sections = content.split(/^---+$/m);
|
|
73
|
-
if (sections.length > 1) {
|
|
74
|
-
for (const section of sections) {
|
|
75
|
-
const trimmed = section.trim();
|
|
76
|
-
if (trimmed.length < 10) continue;
|
|
77
|
-
const paragraphs = splitByParagraphs(trimmed, maxChunkChars);
|
|
78
|
-
for (const p of paragraphs) {
|
|
79
|
-
chunks.push({ content: p, source: '' });
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
} else {
|
|
83
|
-
// No structure — split by paragraphs
|
|
84
|
-
const paragraphs = splitByParagraphs(content, maxChunkChars);
|
|
85
|
-
for (const p of paragraphs) {
|
|
86
|
-
chunks.push({ content: p, source: '' });
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
return chunks;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Split text by double-newline (paragraphs), merging small paragraphs
|
|
96
|
-
* and splitting oversized ones.
|
|
97
|
-
*/
|
|
98
|
-
function splitByParagraphs(text: string, maxChars = 1000): string[] {
|
|
99
|
-
const rawParagraphs = text.split(/\n\n+/);
|
|
100
|
-
const results: string[] = [];
|
|
101
|
-
let buffer = '';
|
|
102
|
-
|
|
103
|
-
for (const para of rawParagraphs) {
|
|
104
|
-
const trimmed = para.trim();
|
|
105
|
-
if (!trimmed) continue;
|
|
106
|
-
|
|
107
|
-
if (buffer.length + trimmed.length + 2 <= maxChars) {
|
|
108
|
-
buffer = buffer ? `${buffer}\n\n${trimmed}` : trimmed;
|
|
109
|
-
} else {
|
|
110
|
-
if (buffer) results.push(buffer);
|
|
111
|
-
if (trimmed.length > maxChars) {
|
|
112
|
-
// Hard split at maxChars boundary
|
|
113
|
-
for (let i = 0; i < trimmed.length; i += maxChars) {
|
|
114
|
-
results.push(trimmed.slice(i, i + maxChars));
|
|
115
|
-
}
|
|
116
|
-
buffer = '';
|
|
117
|
-
} else {
|
|
118
|
-
buffer = trimmed;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (buffer && buffer.length >= 10) results.push(buffer);
|
|
124
|
-
return results;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
/**
|
|
128
|
-
* Small delay helper for rate limiting.
|
|
129
|
-
*/
|
|
130
|
-
function delay(ms: number): Promise<void> {
|
|
131
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Import OpenClaw memory files into Keyoku.
|
|
136
|
-
*/
|
|
137
|
-
export async function importMemoryFiles(params: {
|
|
138
|
-
client: KeyokuClient;
|
|
139
|
-
entityId: string;
|
|
140
|
-
workspaceDir: string;
|
|
141
|
-
agentId?: string;
|
|
142
|
-
dryRun?: boolean;
|
|
143
|
-
logger?: { info: (msg: string) => void; warn: (msg: string) => void };
|
|
144
|
-
}): Promise<ImportResult> {
|
|
145
|
-
const { client, entityId, workspaceDir, agentId, dryRun = false, logger = console } = params;
|
|
146
|
-
const result: ImportResult = { imported: 0, skipped: 0, errors: 0 };
|
|
147
|
-
|
|
148
|
-
// Discover memory files
|
|
149
|
-
const files: { path: string; name: string }[] = [];
|
|
150
|
-
|
|
151
|
-
// Check for MEMORY.md
|
|
152
|
-
const memoryMdPath = join(workspaceDir, 'MEMORY.md');
|
|
153
|
-
if (existsSync(memoryMdPath)) {
|
|
154
|
-
files.push({ path: memoryMdPath, name: 'MEMORY.md' });
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Check for memory/ directory
|
|
158
|
-
const memoryDir = join(workspaceDir, 'memory');
|
|
159
|
-
if (existsSync(memoryDir) && statSync(memoryDir).isDirectory()) {
|
|
160
|
-
const entries = readdirSync(memoryDir)
|
|
161
|
-
.filter((f) => f.endsWith('.md'))
|
|
162
|
-
.sort(); // chronological for dated files
|
|
163
|
-
|
|
164
|
-
for (const entry of entries) {
|
|
165
|
-
files.push({ path: join(memoryDir, entry), name: `memory/${entry}` });
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
if (files.length === 0) {
|
|
170
|
-
logger.info('No memory files found in workspace.');
|
|
171
|
-
return result;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
logger.info(`Found ${files.length} memory file(s) to import.`);
|
|
175
|
-
|
|
176
|
-
// Process each file
|
|
177
|
-
for (const file of files) {
|
|
178
|
-
let content: string;
|
|
179
|
-
try {
|
|
180
|
-
content = readFileSync(file.path, 'utf-8');
|
|
181
|
-
} catch (err) {
|
|
182
|
-
logger.warn(`Failed to read ${file.name}: ${String(err)}`);
|
|
183
|
-
result.errors++;
|
|
184
|
-
continue;
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
if (content.trim().length < 10) {
|
|
188
|
-
logger.info(`Skipping ${file.name} (too short)`);
|
|
189
|
-
result.skipped++;
|
|
190
|
-
continue;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const chunks = chunkByHeadings(content);
|
|
194
|
-
|
|
195
|
-
for (const chunk of chunks) {
|
|
196
|
-
chunk.source = file.name;
|
|
197
|
-
|
|
198
|
-
// Build the content to store — include source context
|
|
199
|
-
const taggedContent = chunk.section
|
|
200
|
-
? `[Imported from ${file.name} — ${chunk.section}]\n${chunk.content}`
|
|
201
|
-
: `[Imported from ${file.name}]\n${chunk.content}`;
|
|
202
|
-
|
|
203
|
-
if (dryRun) {
|
|
204
|
-
logger.info(`[dry-run] Would import: ${taggedContent.slice(0, 80)}...`);
|
|
205
|
-
result.imported++;
|
|
206
|
-
continue;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Dedup check: search for similar content
|
|
210
|
-
try {
|
|
211
|
-
const queryText = chunk.content.slice(0, 100);
|
|
212
|
-
const existing = await client.search(entityId, queryText, { limit: 1, min_score: 0.95 });
|
|
213
|
-
|
|
214
|
-
if (existing.length > 0) {
|
|
215
|
-
result.skipped++;
|
|
216
|
-
continue;
|
|
217
|
-
}
|
|
218
|
-
} catch {
|
|
219
|
-
// Search failed — proceed with import anyway
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Store the memory
|
|
223
|
-
try {
|
|
224
|
-
await client.remember(entityId, taggedContent, {
|
|
225
|
-
agent_id: agentId,
|
|
226
|
-
source: 'migration',
|
|
227
|
-
});
|
|
228
|
-
result.imported++;
|
|
229
|
-
logger.info(`Imported: ${chunk.content.slice(0, 60)}...`);
|
|
230
|
-
} catch (err) {
|
|
231
|
-
logger.warn(`Failed to store chunk from ${file.name}: ${String(err)}`);
|
|
232
|
-
result.errors++;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
// Rate limit
|
|
236
|
-
await delay(50);
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return result;
|
|
241
|
-
}
|
package/src/service.ts
DELETED
|
@@ -1,145 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Keyoku binary lifecycle management.
|
|
3
|
-
* Starts/stops the Keyoku Go binary as a child process.
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
import { spawn, type ChildProcess } from 'node:child_process';
|
|
7
|
-
import { existsSync, mkdirSync } from 'node:fs';
|
|
8
|
-
import { resolve } from 'node:path';
|
|
9
|
-
import { randomBytes } from 'node:crypto';
|
|
10
|
-
import type { PluginApi } from './types.js';
|
|
11
|
-
|
|
12
|
-
let keyokuProcess: ChildProcess | null = null;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Check if Keyoku is already running by attempting a health check.
|
|
16
|
-
*/
|
|
17
|
-
async function isKeyokuRunning(url: string): Promise<boolean> {
|
|
18
|
-
try {
|
|
19
|
-
const controller = new AbortController();
|
|
20
|
-
const timer = setTimeout(() => controller.abort(), 2000);
|
|
21
|
-
const res = await fetch(`${url}/health`, { signal: controller.signal });
|
|
22
|
-
clearTimeout(timer);
|
|
23
|
-
return res.ok;
|
|
24
|
-
} catch {
|
|
25
|
-
return false;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Wait for Keyoku to become healthy, polling every interval up to a timeout.
|
|
31
|
-
*/
|
|
32
|
-
async function waitForHealthy(url: string, timeoutMs = 5000, intervalMs = 500): Promise<boolean> {
|
|
33
|
-
const deadline = Date.now() + timeoutMs;
|
|
34
|
-
while (Date.now() < deadline) {
|
|
35
|
-
if (await isKeyokuRunning(url)) return true;
|
|
36
|
-
await new Promise((r) => setTimeout(r, intervalMs));
|
|
37
|
-
}
|
|
38
|
-
return false;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Find keyoku binary on disk or PATH.
|
|
43
|
-
*/
|
|
44
|
-
function findKeyokuBinary(): string | null {
|
|
45
|
-
const home = process.env.HOME ?? '';
|
|
46
|
-
const candidates = [
|
|
47
|
-
resolve(home, '.keyoku', 'bin', 'keyoku'),
|
|
48
|
-
resolve(home, '.local', 'bin', 'keyoku'),
|
|
49
|
-
];
|
|
50
|
-
|
|
51
|
-
for (const candidate of candidates) {
|
|
52
|
-
if (existsSync(candidate)) return candidate;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// Fall back to PATH resolution
|
|
56
|
-
return 'keyoku';
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Ensure the Keyoku data directory exists and return the DB path.
|
|
61
|
-
*/
|
|
62
|
-
function ensureDataDir(): string {
|
|
63
|
-
const dir = resolve(process.env.HOME ?? '', '.keyoku', 'data');
|
|
64
|
-
mkdirSync(dir, { recursive: true });
|
|
65
|
-
return resolve(dir, 'keyoku.db');
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
export function registerService(api: PluginApi, keyokuUrl: string): void {
|
|
69
|
-
api.registerService({
|
|
70
|
-
id: 'keyoku-engine',
|
|
71
|
-
|
|
72
|
-
async start() {
|
|
73
|
-
// Skip if already running
|
|
74
|
-
if (await isKeyokuRunning(keyokuUrl)) {
|
|
75
|
-
api.logger.info('keyoku: Keyoku already running');
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
const binary = findKeyokuBinary();
|
|
80
|
-
if (!binary) {
|
|
81
|
-
api.logger.warn('keyoku: Keyoku binary not found — memory features require Keyoku to be running');
|
|
82
|
-
return;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Prepare environment
|
|
86
|
-
const env = { ...process.env };
|
|
87
|
-
if (!env.KEYOKU_SESSION_TOKEN) {
|
|
88
|
-
env.KEYOKU_SESSION_TOKEN = randomBytes(16).toString('hex');
|
|
89
|
-
api.logger.info('keyoku: Generated session token');
|
|
90
|
-
}
|
|
91
|
-
if (!env.KEYOKU_DB_PATH) {
|
|
92
|
-
env.KEYOKU_DB_PATH = ensureDataDir();
|
|
93
|
-
api.logger.info(`keyoku: Using database at ${env.KEYOKU_DB_PATH}`);
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
try {
|
|
97
|
-
keyokuProcess = spawn(binary, [], {
|
|
98
|
-
stdio: ['ignore', 'pipe', 'pipe'],
|
|
99
|
-
detached: false,
|
|
100
|
-
env,
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
// Pipe stdout/stderr to logger
|
|
104
|
-
keyokuProcess.stdout?.on('data', (data: Buffer) => {
|
|
105
|
-
const line = data.toString().trim();
|
|
106
|
-
if (line) api.logger.info(`keyoku: ${line}`);
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
keyokuProcess.stderr?.on('data', (data: Buffer) => {
|
|
110
|
-
const line = data.toString().trim();
|
|
111
|
-
if (line) api.logger.warn(`keyoku: ${line}`);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
keyokuProcess.on('error', (err) => {
|
|
115
|
-
api.logger.warn(`keyoku: Failed to start Keyoku: ${err.message}`);
|
|
116
|
-
keyokuProcess = null;
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
keyokuProcess.on('exit', (code) => {
|
|
120
|
-
if (code !== 0 && code !== null) {
|
|
121
|
-
api.logger.warn(`keyoku: Keyoku exited with code ${code}`);
|
|
122
|
-
}
|
|
123
|
-
keyokuProcess = null;
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
// Wait for health check with retry
|
|
127
|
-
if (await waitForHealthy(keyokuUrl)) {
|
|
128
|
-
api.logger.info('keyoku: Keyoku started successfully');
|
|
129
|
-
} else {
|
|
130
|
-
api.logger.warn('keyoku: Keyoku started but health check failed — it may still be initializing');
|
|
131
|
-
}
|
|
132
|
-
} catch (err) {
|
|
133
|
-
api.logger.warn(`keyoku: Could not start Keyoku: ${String(err)}`);
|
|
134
|
-
}
|
|
135
|
-
},
|
|
136
|
-
|
|
137
|
-
stop() {
|
|
138
|
-
if (keyokuProcess) {
|
|
139
|
-
keyokuProcess.kill('SIGTERM');
|
|
140
|
-
keyokuProcess = null;
|
|
141
|
-
api.logger.info('keyoku: Keyoku stopped');
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
});
|
|
145
|
-
}
|
package/src/tools.ts
DELETED
|
@@ -1,239 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* OpenClaw tool registrations for Keyoku memory operations.
|
|
3
|
-
* Registers 7 tools:
|
|
4
|
-
* memory_search, memory_get (OpenClaw standard — replaces built-in file-based memory)
|
|
5
|
-
* memory_store, memory_forget, memory_stats (Keyoku memory management)
|
|
6
|
-
* schedule_create, schedule_list (Keyoku scheduling)
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
import { Type } from '@sinclair/typebox';
|
|
10
|
-
import type { KeyokuClient } from '@keyoku/memory';
|
|
11
|
-
import type { PluginApi } from './types.js';
|
|
12
|
-
|
|
13
|
-
export function registerTools(api: PluginApi, client: KeyokuClient, entityId: string, agentId: string): void {
|
|
14
|
-
// memory_search — OpenClaw-standard search tool (replaces memory-core's built-in)
|
|
15
|
-
api.registerTool(
|
|
16
|
-
{
|
|
17
|
-
name: 'memory_search',
|
|
18
|
-
label: 'Memory Search',
|
|
19
|
-
description:
|
|
20
|
-
'Search through memories for relevant information. Returns semantically similar memories ranked by relevance.',
|
|
21
|
-
parameters: Type.Object({
|
|
22
|
-
query: Type.String({ description: 'Search query' }),
|
|
23
|
-
maxResults: Type.Optional(Type.Number({ description: 'Max results (default: 5)' })),
|
|
24
|
-
minScore: Type.Optional(Type.Number({ description: 'Minimum relevance score 0-1' })),
|
|
25
|
-
}),
|
|
26
|
-
async execute(_toolCallId, params) {
|
|
27
|
-
const { query, maxResults = 5, minScore = 0.1 } = params as {
|
|
28
|
-
query: string;
|
|
29
|
-
maxResults?: number;
|
|
30
|
-
minScore?: number;
|
|
31
|
-
};
|
|
32
|
-
const results = await client.search(entityId, query, { limit: maxResults, min_score: minScore });
|
|
33
|
-
|
|
34
|
-
if (results.length === 0) {
|
|
35
|
-
return {
|
|
36
|
-
content: [{ type: 'text', text: JSON.stringify({ results: [], provider: 'memory', mode: 'semantic' }) }],
|
|
37
|
-
details: { count: 0 },
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
const mapped = results.map((r) => ({
|
|
42
|
-
path: `mem:${r.memory.id}`,
|
|
43
|
-
startLine: 1,
|
|
44
|
-
endLine: 1,
|
|
45
|
-
score: r.similarity,
|
|
46
|
-
snippet: r.memory.content,
|
|
47
|
-
source: 'memory',
|
|
48
|
-
citation: `mem:${r.memory.id}`,
|
|
49
|
-
}));
|
|
50
|
-
|
|
51
|
-
return {
|
|
52
|
-
content: [
|
|
53
|
-
{ type: 'text', text: JSON.stringify({ results: mapped, provider: 'memory', mode: 'semantic' }) },
|
|
54
|
-
],
|
|
55
|
-
details: { count: mapped.length },
|
|
56
|
-
};
|
|
57
|
-
},
|
|
58
|
-
},
|
|
59
|
-
{ name: 'memory_search' },
|
|
60
|
-
);
|
|
61
|
-
|
|
62
|
-
// memory_get — OpenClaw-standard memory read tool (replaces file-based reads)
|
|
63
|
-
api.registerTool(
|
|
64
|
-
{
|
|
65
|
-
name: 'memory_get',
|
|
66
|
-
label: 'Memory Get',
|
|
67
|
-
description:
|
|
68
|
-
'Read a specific memory by its ID (mem:<id>) or search for a memory by keyword.',
|
|
69
|
-
parameters: Type.Object({
|
|
70
|
-
path: Type.String({ description: 'Memory path (mem:<id>) or keyword to search' }),
|
|
71
|
-
from: Type.Optional(Type.Number({ description: 'Line offset (unused)' })),
|
|
72
|
-
lines: Type.Optional(Type.Number({ description: 'Line count (unused)' })),
|
|
73
|
-
}),
|
|
74
|
-
async execute(_toolCallId, params) {
|
|
75
|
-
const { path: memPath } = params as { path: string; from?: number; lines?: number };
|
|
76
|
-
|
|
77
|
-
if (memPath.startsWith('mem:') || memPath.startsWith('keyoku:')) {
|
|
78
|
-
const id = memPath.startsWith('mem:') ? memPath.slice(4) : memPath.slice(7);
|
|
79
|
-
try {
|
|
80
|
-
const memory = await client.getMemory(id);
|
|
81
|
-
return {
|
|
82
|
-
content: [{ type: 'text', text: JSON.stringify({ text: memory.content, path: memPath }) }],
|
|
83
|
-
};
|
|
84
|
-
} catch {
|
|
85
|
-
return {
|
|
86
|
-
content: [{ type: 'text', text: JSON.stringify({ text: '', path: memPath, error: 'Memory not found' }) }],
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// Fallback: treat path as a search query
|
|
92
|
-
const results = await client.search(entityId, memPath, { limit: 1 });
|
|
93
|
-
if (results.length > 0) {
|
|
94
|
-
return {
|
|
95
|
-
content: [
|
|
96
|
-
{
|
|
97
|
-
type: 'text',
|
|
98
|
-
text: JSON.stringify({
|
|
99
|
-
text: results[0].memory.content,
|
|
100
|
-
path: `mem:${results[0].memory.id}`,
|
|
101
|
-
}),
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
return {
|
|
108
|
-
content: [{ type: 'text', text: JSON.stringify({ text: '', path: memPath, error: 'Not found' }) }],
|
|
109
|
-
};
|
|
110
|
-
},
|
|
111
|
-
},
|
|
112
|
-
{ name: 'memory_get' },
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
// memory_store — store a new memory
|
|
116
|
-
api.registerTool(
|
|
117
|
-
{
|
|
118
|
-
name: 'memory_store',
|
|
119
|
-
label: 'Memory Store',
|
|
120
|
-
description:
|
|
121
|
-
'Save important information in long-term memory. Use for preferences, facts, decisions.',
|
|
122
|
-
parameters: Type.Object({
|
|
123
|
-
text: Type.String({ description: 'Information to remember' }),
|
|
124
|
-
}),
|
|
125
|
-
async execute(_toolCallId, params) {
|
|
126
|
-
const { text } = params as { text: string };
|
|
127
|
-
const result = await client.remember(entityId, text, { agent_id: agentId });
|
|
128
|
-
|
|
129
|
-
return {
|
|
130
|
-
content: [{ type: 'text', text: `Stored: "${text.slice(0, 100)}${text.length > 100 ? '...' : ''}"` }],
|
|
131
|
-
details: { memories_created: result.memories_created },
|
|
132
|
-
};
|
|
133
|
-
},
|
|
134
|
-
},
|
|
135
|
-
{ name: 'memory_store' },
|
|
136
|
-
);
|
|
137
|
-
|
|
138
|
-
// memory_forget — delete a memory by ID
|
|
139
|
-
api.registerTool(
|
|
140
|
-
{
|
|
141
|
-
name: 'memory_forget',
|
|
142
|
-
label: 'Memory Forget',
|
|
143
|
-
description: 'Delete a specific memory by ID.',
|
|
144
|
-
parameters: Type.Object({
|
|
145
|
-
memory_id: Type.String({ description: 'The memory ID to delete' }),
|
|
146
|
-
}),
|
|
147
|
-
async execute(_toolCallId, params) {
|
|
148
|
-
const { memory_id } = params as { memory_id: string };
|
|
149
|
-
const result = await client.deleteMemory(memory_id);
|
|
150
|
-
|
|
151
|
-
return {
|
|
152
|
-
content: [{ type: 'text', text: `Memory ${memory_id} deleted.` }],
|
|
153
|
-
details: { status: result.status },
|
|
154
|
-
};
|
|
155
|
-
},
|
|
156
|
-
},
|
|
157
|
-
{ name: 'memory_forget' },
|
|
158
|
-
);
|
|
159
|
-
|
|
160
|
-
// memory_stats — get memory statistics
|
|
161
|
-
api.registerTool(
|
|
162
|
-
{
|
|
163
|
-
name: 'memory_stats',
|
|
164
|
-
label: 'Memory Stats',
|
|
165
|
-
description: 'Get memory statistics for the current entity.',
|
|
166
|
-
parameters: Type.Object({}),
|
|
167
|
-
async execute() {
|
|
168
|
-
const stats = await client.getStats(entityId);
|
|
169
|
-
|
|
170
|
-
const text = [
|
|
171
|
-
`Total memories: ${stats.total_memories}`,
|
|
172
|
-
`Active memories: ${stats.active_memories}`,
|
|
173
|
-
`By type: ${JSON.stringify(stats.by_type)}`,
|
|
174
|
-
`By state: ${JSON.stringify(stats.by_state)}`,
|
|
175
|
-
].join('\n');
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
content: [{ type: 'text', text }],
|
|
179
|
-
details: { ...stats } as Record<string, unknown>,
|
|
180
|
-
};
|
|
181
|
-
},
|
|
182
|
-
},
|
|
183
|
-
{ name: 'memory_stats' },
|
|
184
|
-
);
|
|
185
|
-
|
|
186
|
-
// schedule_create — create a scheduled memory
|
|
187
|
-
api.registerTool(
|
|
188
|
-
{
|
|
189
|
-
name: 'schedule_create',
|
|
190
|
-
label: 'Schedule Create',
|
|
191
|
-
description:
|
|
192
|
-
'Create a scheduled task/reminder. Cron tags: "daily", "weekly", "monthly", or a cron expression.',
|
|
193
|
-
parameters: Type.Object({
|
|
194
|
-
content: Type.String({ description: 'What to schedule' }),
|
|
195
|
-
cron_tag: Type.String({ description: 'Cron tag: "daily", "weekly", "monthly", or cron expression' }),
|
|
196
|
-
}),
|
|
197
|
-
async execute(_toolCallId, params) {
|
|
198
|
-
const { content, cron_tag } = params as { content: string; cron_tag: string };
|
|
199
|
-
const result = await client.createSchedule(entityId, agentId, content, cron_tag);
|
|
200
|
-
|
|
201
|
-
return {
|
|
202
|
-
content: [{ type: 'text', text: `Scheduled: "${content}" (${cron_tag})` }],
|
|
203
|
-
details: { id: result.id },
|
|
204
|
-
};
|
|
205
|
-
},
|
|
206
|
-
},
|
|
207
|
-
{ name: 'schedule_create' },
|
|
208
|
-
);
|
|
209
|
-
|
|
210
|
-
// schedule_list — list schedules
|
|
211
|
-
api.registerTool(
|
|
212
|
-
{
|
|
213
|
-
name: 'schedule_list',
|
|
214
|
-
label: 'Schedule List',
|
|
215
|
-
description: 'List active schedules.',
|
|
216
|
-
parameters: Type.Object({}),
|
|
217
|
-
async execute() {
|
|
218
|
-
const schedules = await client.listSchedules(entityId, agentId);
|
|
219
|
-
|
|
220
|
-
if (schedules.length === 0) {
|
|
221
|
-
return {
|
|
222
|
-
content: [{ type: 'text', text: 'No active schedules.' }],
|
|
223
|
-
details: { count: 0 },
|
|
224
|
-
};
|
|
225
|
-
}
|
|
226
|
-
|
|
227
|
-
const text = schedules
|
|
228
|
-
.map((s, i) => `${i + 1}. ${s.content} (id: ${s.id})`)
|
|
229
|
-
.join('\n');
|
|
230
|
-
|
|
231
|
-
return {
|
|
232
|
-
content: [{ type: 'text', text: `${schedules.length} schedules:\n\n${text}` }],
|
|
233
|
-
details: { count: schedules.length },
|
|
234
|
-
};
|
|
235
|
-
},
|
|
236
|
-
},
|
|
237
|
-
{ name: 'schedule_list' },
|
|
238
|
-
);
|
|
239
|
-
}
|
package/src/types.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Minimal OpenClaw plugin API types.
|
|
3
|
-
* These mirror the types from openclaw/src/plugins/types.ts
|
|
4
|
-
* so that @keyoku/openclaw can compile without importing openclaw directly.
|
|
5
|
-
* At runtime, the actual OpenClaw API object is passed in.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
export type PluginLogger = {
|
|
9
|
-
debug?: (message: string) => void;
|
|
10
|
-
info: (message: string) => void;
|
|
11
|
-
warn: (message: string) => void;
|
|
12
|
-
error: (message: string) => void;
|
|
13
|
-
};
|
|
14
|
-
|
|
15
|
-
export type ToolResult = {
|
|
16
|
-
content: Array<{ type: string; text: string }>;
|
|
17
|
-
details?: Record<string, unknown>;
|
|
18
|
-
};
|
|
19
|
-
|
|
20
|
-
export type AgentTool = {
|
|
21
|
-
name: string;
|
|
22
|
-
label?: string;
|
|
23
|
-
description: string;
|
|
24
|
-
parameters: unknown;
|
|
25
|
-
execute: (toolCallId: string, params: Record<string, unknown>) => Promise<ToolResult>;
|
|
26
|
-
};
|
|
27
|
-
|
|
28
|
-
export type PluginApi = {
|
|
29
|
-
id: string;
|
|
30
|
-
name: string;
|
|
31
|
-
logger: PluginLogger;
|
|
32
|
-
pluginConfig?: Record<string, unknown>;
|
|
33
|
-
registerTool: (tool: AgentTool, opts?: { name?: string; names?: string[] }) => void;
|
|
34
|
-
registerHook?: (events: string | string[], handler: (...args: unknown[]) => unknown, opts?: Record<string, unknown>) => void;
|
|
35
|
-
registerCli: (registrar: (ctx: { program: unknown; config: unknown; logger: PluginLogger }) => void, opts?: { commands?: string[] }) => void;
|
|
36
|
-
registerService: (service: { id: string; start: (ctx: unknown) => void | Promise<void>; stop?: (ctx: unknown) => void | Promise<void> }) => void;
|
|
37
|
-
resolvePath: (input: string) => string;
|
|
38
|
-
on: (hookName: string, handler: (...args: unknown[]) => unknown, opts?: { priority?: number }) => void;
|
|
39
|
-
config?: Record<string, unknown>;
|
|
40
|
-
};
|