@hippodid/openclaw-plugin 1.0.8 → 1.0.10
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 +41 -5
- package/dist/hippodid-client.d.ts.map +1 -1
- package/dist/hippodid-client.js +12 -3
- package/dist/hippodid-client.js.map +1 -1
- package/dist/hooks/auto-capture.d.ts.map +1 -1
- package/dist/hooks/auto-capture.js +40 -6
- package/dist/hooks/auto-capture.js.map +1 -1
- package/dist/hooks/auto-recall.d.ts.map +1 -1
- package/dist/hooks/auto-recall.js +28 -5
- package/dist/hooks/auto-recall.js.map +1 -1
- package/dist/hooks/memory-flush.d.ts.map +1 -1
- package/dist/hooks/memory-flush.js +11 -6
- package/dist/hooks/memory-flush.js.map +1 -1
- package/dist/hooks/session-lifecycle.d.ts.map +1 -1
- package/dist/hooks/session-lifecycle.js +25 -5
- package/dist/hooks/session-lifecycle.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +133 -75
- package/dist/index.js.map +1 -1
- package/dist/tier-manager.d.ts.map +1 -1
- package/dist/tier-manager.js +2 -5
- package/dist/tier-manager.js.map +1 -1
- package/dist/types.d.ts +6 -3
- package/dist/types.d.ts.map +1 -1
- package/package.json +14 -7
- package/skills/hippodid/SKILL.md +4 -4
- package/src/hippodid-client.ts +15 -3
- package/src/hooks/auto-capture.ts +59 -19
- package/src/hooks/auto-recall.ts +48 -20
- package/src/hooks/memory-flush.ts +15 -6
- package/src/hooks/session-lifecycle.ts +34 -5
- package/src/index.ts +165 -110
- package/src/tier-manager.ts +2 -4
- package/src/types.ts +7 -4
|
@@ -4,11 +4,20 @@ export function createMemoryFlushHook(
|
|
|
4
4
|
fileSync: FileSync,
|
|
5
5
|
logger: { info(msg: string): void; warn(msg: string): void },
|
|
6
6
|
): (api: any) => void {
|
|
7
|
-
return (
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
return (api: any) => {
|
|
8
|
+
api.on('before_compaction', async () => {
|
|
9
|
+
try {
|
|
10
|
+
const { synced, changed } = await fileSync.flushNow();
|
|
11
|
+
logger.info(
|
|
12
|
+
`hippodid: flushed ${synced} files before compaction (${changed} changed)`,
|
|
13
|
+
);
|
|
14
|
+
} catch (e) {
|
|
15
|
+
logger.warn(
|
|
16
|
+
`hippodid: compaction flush failed: ${e instanceof Error ? e.message : 'unknown'}`,
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
logger.info('hippodid: memory flush handler registered');
|
|
13
22
|
};
|
|
14
23
|
}
|
|
@@ -7,10 +7,39 @@ export function createSessionHooks(
|
|
|
7
7
|
autoRecall: boolean,
|
|
8
8
|
logger: { info(msg: string): void; warn(msg: string): void },
|
|
9
9
|
): (api: any) => void {
|
|
10
|
-
return (
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
10
|
+
return (api: any) => {
|
|
11
|
+
api.on('session_start', async () => {
|
|
12
|
+
try {
|
|
13
|
+
await tierManager.initialize();
|
|
14
|
+
if (!tierManager.shouldHydrateOnStart(autoRecall)) {
|
|
15
|
+
logger.info(
|
|
16
|
+
'hippodid: session start hydration skipped because auto-recall is active',
|
|
17
|
+
);
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const hydrated = await fileSync.hydrateFromCloud();
|
|
22
|
+
logger.info(`hippodid: session started, hydrated ${hydrated} files`);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
logger.warn(
|
|
25
|
+
`hippodid: session start error: ${e instanceof Error ? e.message : 'unknown'}`,
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
api.on('session_end', async () => {
|
|
31
|
+
try {
|
|
32
|
+
const { synced, changed } = await fileSync.flushNow();
|
|
33
|
+
logger.info(
|
|
34
|
+
`hippodid: session ended, flushed ${synced} files (${changed} changed)`,
|
|
35
|
+
);
|
|
36
|
+
} catch (e) {
|
|
37
|
+
logger.warn(
|
|
38
|
+
`hippodid: session end error: ${e instanceof Error ? e.message : 'unknown'}`,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
logger.info('hippodid: session lifecycle hooks registered');
|
|
15
44
|
};
|
|
16
45
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { resolve } from 'node:path';
|
|
3
4
|
import type { PluginConfig } from './types.js';
|
|
4
5
|
import { createClient, type HippoDidClient } from './hippodid-client.js';
|
|
@@ -10,39 +11,42 @@ import { createSessionHooks } from './hooks/session-lifecycle.js';
|
|
|
10
11
|
import { createAutoRecallHook } from './hooks/auto-recall.js';
|
|
11
12
|
import { createAutoCaptureHook } from './hooks/auto-capture.js';
|
|
12
13
|
|
|
13
|
-
const
|
|
14
|
+
const require = createRequire(import.meta.url);
|
|
15
|
+
const VERSION =
|
|
16
|
+
((require('../package.json') as { version?: string }).version) ?? '0.0.0-dev';
|
|
17
|
+
let hasInitialized = false;
|
|
14
18
|
|
|
15
19
|
export default {
|
|
16
20
|
id: 'hippodid',
|
|
17
21
|
|
|
18
22
|
register(api: any): void {
|
|
19
23
|
try {
|
|
20
|
-
|
|
24
|
+
if (hasInitialized) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const config = resolveConfig(api.pluginConfig ?? api.config ?? {});
|
|
21
29
|
const logger = api.logger ?? {
|
|
22
30
|
info: (msg: string) => console.log(msg),
|
|
23
31
|
warn: (msg: string) => console.warn(msg),
|
|
24
32
|
error: (msg: string) => console.error(msg),
|
|
25
33
|
};
|
|
26
34
|
|
|
27
|
-
const apiKey = config
|
|
28
|
-
const characterId = config
|
|
29
|
-
|
|
35
|
+
const apiKey = config.apiKey.trim();
|
|
36
|
+
const characterId = config.characterId.trim();
|
|
30
37
|
if (!apiKey || !characterId) {
|
|
31
|
-
logger.warn(
|
|
38
|
+
logger.warn(
|
|
39
|
+
'HippoDid: apiKey and characterId required — configure in openclaw.json',
|
|
40
|
+
);
|
|
32
41
|
return;
|
|
33
42
|
}
|
|
34
43
|
|
|
44
|
+
hasInitialized = true;
|
|
45
|
+
|
|
35
46
|
const client = createClient(apiKey, config.baseUrl);
|
|
36
|
-
const tierManager = createTierManager(
|
|
37
|
-
client,
|
|
38
|
-
characterId,
|
|
39
|
-
logger,
|
|
40
|
-
);
|
|
47
|
+
const tierManager = createTierManager(client, characterId, logger);
|
|
41
48
|
const watchPaths = resolveWatchPaths(config);
|
|
42
|
-
const effectiveSyncInterval = Math.max(
|
|
43
|
-
config.syncIntervalSeconds,
|
|
44
|
-
60,
|
|
45
|
-
);
|
|
49
|
+
const effectiveSyncInterval = Math.max(config.syncIntervalSeconds, 60);
|
|
46
50
|
const fileSync = createFileSync(
|
|
47
51
|
client,
|
|
48
52
|
config,
|
|
@@ -133,6 +137,7 @@ function resolveConfig(raw: any): PluginConfig {
|
|
|
133
137
|
additionalPaths: [],
|
|
134
138
|
};
|
|
135
139
|
}
|
|
140
|
+
|
|
136
141
|
return {
|
|
137
142
|
apiKey: raw.apiKey ?? '',
|
|
138
143
|
characterId: raw.characterId ?? '',
|
|
@@ -152,49 +157,162 @@ function registerCommands(
|
|
|
152
157
|
tierManager: TierManager,
|
|
153
158
|
logger: { info(msg: string): void; warn(msg: string): void },
|
|
154
159
|
): void {
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
160
|
+
const getStatusText = async (): Promise<string> => {
|
|
161
|
+
const tier = tierManager.getCurrentTier();
|
|
162
|
+
const statusResult = await client.getSyncStatus(config.characterId);
|
|
163
|
+
const lines = [
|
|
164
|
+
'HippoDid status:',
|
|
165
|
+
`- Character: ${config.characterId}`,
|
|
166
|
+
`- Tier: ${tier.tier}`,
|
|
167
|
+
`- Auto-Recall: ${tier.features.autoRecallAvailable ? 'available' : 'unavailable'} (config: ${config.autoRecall ? 'ON' : 'OFF'})`,
|
|
168
|
+
`- Auto-Capture: ${tier.features.autoCaptureAvailable ? 'available' : 'unavailable'} (config: ${config.autoCapture ? 'ON' : 'OFF'})`,
|
|
169
|
+
];
|
|
170
|
+
|
|
171
|
+
if (statusResult.ok) {
|
|
172
|
+
lines.push(`- Synced sources: ${statusResult.value.entries.length}`);
|
|
173
|
+
for (const entry of statusResult.value.entries) {
|
|
174
|
+
lines.push(
|
|
175
|
+
` - ${entry.sourcePath} (${entry.label}) — last sync: ${entry.lastSyncedAt}`,
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
} else {
|
|
179
|
+
lines.push(
|
|
180
|
+
`- Sync status: unavailable (${statusResult.error.message})`,
|
|
169
181
|
);
|
|
182
|
+
}
|
|
170
183
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
184
|
+
return lines.join('\n');
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const runImport = async (workspaceOverride?: string): Promise<string> => {
|
|
188
|
+
const { readdir } = await import('node:fs/promises');
|
|
189
|
+
const { join, extname } = await import('node:path');
|
|
190
|
+
const { createHash } = await import('node:crypto');
|
|
191
|
+
|
|
192
|
+
const workspacePath = workspaceOverride
|
|
193
|
+
? resolve(workspaceOverride)
|
|
194
|
+
: resolve(process.cwd());
|
|
195
|
+
|
|
196
|
+
const memoryDir = join(workspacePath, 'memory');
|
|
197
|
+
const memoryMd = join(workspacePath, 'MEMORY.md');
|
|
198
|
+
const filesToImport: Array<{ path: string; label: string }> = [];
|
|
199
|
+
|
|
200
|
+
try {
|
|
201
|
+
const entries = await readdir(memoryDir);
|
|
202
|
+
for (const entry of entries) {
|
|
203
|
+
if (extname(entry) === '.md') {
|
|
204
|
+
filesToImport.push({
|
|
205
|
+
path: join(memoryDir, entry),
|
|
206
|
+
label: 'workspace-memory',
|
|
207
|
+
});
|
|
177
208
|
}
|
|
178
|
-
}
|
|
209
|
+
}
|
|
210
|
+
} catch {
|
|
211
|
+
// memory dir may not exist
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
try {
|
|
215
|
+
await readFile(memoryMd);
|
|
216
|
+
filesToImport.push({ path: memoryMd, label: 'MEMORY.md' });
|
|
217
|
+
} catch {
|
|
218
|
+
// MEMORY.md may not exist
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
if (filesToImport.length === 0) {
|
|
222
|
+
return 'hippodid: no memory files found to import';
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
logger.info(
|
|
226
|
+
`hippodid: importing ${filesToImport.length} files from ${workspacePath}...`,
|
|
227
|
+
);
|
|
228
|
+
|
|
229
|
+
let imported = 0;
|
|
230
|
+
for (const file of filesToImport) {
|
|
231
|
+
try {
|
|
232
|
+
const content = await readFile(file.path);
|
|
233
|
+
const hash = createHash('sha256').update(content).digest('hex');
|
|
234
|
+
const base64 = content.toString('base64');
|
|
235
|
+
const result = await client.syncFile(
|
|
236
|
+
config.characterId,
|
|
237
|
+
file.path,
|
|
238
|
+
file.label,
|
|
239
|
+
base64,
|
|
240
|
+
hash,
|
|
241
|
+
);
|
|
242
|
+
if (result.ok) imported++;
|
|
243
|
+
} catch (e) {
|
|
179
244
|
logger.warn(
|
|
180
|
-
`
|
|
245
|
+
`hippodid: import failed for ${file.path}: ${e instanceof Error ? e.message : 'unknown'}`,
|
|
181
246
|
);
|
|
182
247
|
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return `hippodid: import complete — ${imported}/${filesToImport.length} files imported`;
|
|
251
|
+
};
|
|
252
|
+
|
|
253
|
+
api.registerCommand({
|
|
254
|
+
name: 'hippodid',
|
|
255
|
+
description: 'Show HippoDid status and run sync/import actions.',
|
|
256
|
+
acceptsArgs: true,
|
|
257
|
+
handler: async (ctx: any) => {
|
|
258
|
+
const args = ctx?.args?.trim() ?? '';
|
|
259
|
+
const [action = 'status', ...rest] = args.split(/\s+/).filter(Boolean);
|
|
260
|
+
|
|
261
|
+
if (action === 'status') {
|
|
262
|
+
return { text: await getStatusText() };
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (action === 'sync') {
|
|
266
|
+
logger.info('hippodid: manual sync triggered...');
|
|
267
|
+
const { synced, changed } = await fileSync.flushNow();
|
|
268
|
+
return {
|
|
269
|
+
text: `hippodid: manual sync complete — ${synced} files (${changed} changed)`,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if (action === 'import') {
|
|
274
|
+
const workspace = rest.join(' ').trim() || undefined;
|
|
275
|
+
return { text: await runImport(workspace) };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
return {
|
|
279
|
+
text: [
|
|
280
|
+
'HippoDid commands:',
|
|
281
|
+
'',
|
|
282
|
+
'/hippodid status',
|
|
283
|
+
'/hippodid sync',
|
|
284
|
+
'/hippodid import [workspace-path]',
|
|
285
|
+
].join('\n'),
|
|
286
|
+
};
|
|
287
|
+
},
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
api.registerTool({
|
|
291
|
+
name: 'hippodid:status',
|
|
292
|
+
description: 'Show HippoDid tier, sync status, and watched paths',
|
|
293
|
+
execute: async () => {
|
|
294
|
+
const text = await getStatusText();
|
|
295
|
+
for (const line of text.split('\n')) {
|
|
296
|
+
logger.info(line);
|
|
297
|
+
}
|
|
298
|
+
return text;
|
|
183
299
|
},
|
|
184
300
|
});
|
|
185
301
|
|
|
186
|
-
api.registerTool(
|
|
302
|
+
api.registerTool({
|
|
303
|
+
name: 'hippodid:sync',
|
|
187
304
|
description: 'Trigger immediate sync of all watched files',
|
|
188
|
-
|
|
305
|
+
execute: async () => {
|
|
189
306
|
logger.info('hippodid: manual sync triggered...');
|
|
190
307
|
const { synced, changed } = await fileSync.flushNow();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
308
|
+
const text = `hippodid: manual sync complete — ${synced} files (${changed} changed)`;
|
|
309
|
+
logger.info(text);
|
|
310
|
+
return text;
|
|
194
311
|
},
|
|
195
312
|
});
|
|
196
313
|
|
|
197
|
-
api.registerTool(
|
|
314
|
+
api.registerTool({
|
|
315
|
+
name: 'hippodid:import',
|
|
198
316
|
description: 'Import existing workspace memory into HippoDid character',
|
|
199
317
|
args: [
|
|
200
318
|
{
|
|
@@ -203,73 +321,10 @@ function registerCommands(
|
|
|
203
321
|
required: false,
|
|
204
322
|
},
|
|
205
323
|
],
|
|
206
|
-
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
const workspacePath = args['workspace']
|
|
212
|
-
? resolve(args['workspace'])
|
|
213
|
-
: resolve(process.cwd());
|
|
214
|
-
|
|
215
|
-
const memoryDir = join(workspacePath, 'memory');
|
|
216
|
-
const memoryMd = join(workspacePath, 'MEMORY.md');
|
|
217
|
-
const filesToImport: Array<{ path: string; label: string }> = [];
|
|
218
|
-
|
|
219
|
-
try {
|
|
220
|
-
const entries = await readdir(memoryDir);
|
|
221
|
-
for (const entry of entries) {
|
|
222
|
-
if (extname(entry) === '.md') {
|
|
223
|
-
filesToImport.push({
|
|
224
|
-
path: join(memoryDir, entry),
|
|
225
|
-
label: 'workspace-memory',
|
|
226
|
-
});
|
|
227
|
-
}
|
|
228
|
-
}
|
|
229
|
-
} catch {
|
|
230
|
-
// memory dir may not exist
|
|
231
|
-
}
|
|
232
|
-
|
|
233
|
-
try {
|
|
234
|
-
await readFile(memoryMd);
|
|
235
|
-
filesToImport.push({ path: memoryMd, label: 'MEMORY.md' });
|
|
236
|
-
} catch {
|
|
237
|
-
// MEMORY.md may not exist
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
if (filesToImport.length === 0) {
|
|
241
|
-
logger.info('hippodid: no memory files found to import');
|
|
242
|
-
return;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
logger.info(
|
|
246
|
-
`hippodid: importing ${filesToImport.length} files from ${workspacePath}...`,
|
|
247
|
-
);
|
|
248
|
-
|
|
249
|
-
let imported = 0;
|
|
250
|
-
for (const file of filesToImport) {
|
|
251
|
-
try {
|
|
252
|
-
const content = await readFile(file.path);
|
|
253
|
-
const hash = createHash('sha256').update(content).digest('hex');
|
|
254
|
-
const base64 = content.toString('base64');
|
|
255
|
-
const result = await client.syncFile(
|
|
256
|
-
config.characterId,
|
|
257
|
-
file.path,
|
|
258
|
-
file.label,
|
|
259
|
-
base64,
|
|
260
|
-
hash,
|
|
261
|
-
);
|
|
262
|
-
if (result.ok) imported++;
|
|
263
|
-
} catch (e) {
|
|
264
|
-
logger.warn(
|
|
265
|
-
`hippodid: import failed for ${file.path}: ${e instanceof Error ? e.message : 'unknown'}`,
|
|
266
|
-
);
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
|
-
logger.info(
|
|
271
|
-
`hippodid: import complete — ${imported}/${filesToImport.length} files imported`,
|
|
272
|
-
);
|
|
324
|
+
execute: async (args: Record<string, string>) => {
|
|
325
|
+
const text = await runImport(args['workspace']);
|
|
326
|
+
logger.info(text);
|
|
327
|
+
return text;
|
|
273
328
|
},
|
|
274
329
|
});
|
|
275
330
|
}
|
package/src/tier-manager.ts
CHANGED
|
@@ -52,9 +52,7 @@ export function createTierManager(
|
|
|
52
52
|
},
|
|
53
53
|
|
|
54
54
|
shouldMountFileSync(autoCaptureEnabled: boolean): boolean {
|
|
55
|
-
|
|
56
|
-
if (isFree) return true;
|
|
57
|
-
return !autoCaptureEnabled;
|
|
55
|
+
return !this.shouldMountAutoCapture(autoCaptureEnabled);
|
|
58
56
|
},
|
|
59
57
|
|
|
60
58
|
shouldMountAutoRecall(autoRecallEnabled: boolean): boolean {
|
|
@@ -76,7 +74,7 @@ export function createTierManager(
|
|
|
76
74
|
shouldHydrateOnStart(autoRecallEnabled: boolean): boolean {
|
|
77
75
|
const isFree = !isPaidTier(currentTier);
|
|
78
76
|
if (isFree) return true;
|
|
79
|
-
return !autoRecallEnabled;
|
|
77
|
+
return !this.shouldMountAutoRecall(autoRecallEnabled);
|
|
80
78
|
},
|
|
81
79
|
|
|
82
80
|
getEffectiveSyncInterval(configInterval: number): number {
|
package/src/types.ts
CHANGED
|
@@ -59,9 +59,12 @@ export interface TierInfo {
|
|
|
59
59
|
export interface TierApiResponse {
|
|
60
60
|
tier: string;
|
|
61
61
|
features: {
|
|
62
|
-
auto_recall_available
|
|
63
|
-
auto_capture_available
|
|
64
|
-
min_sync_interval_seconds
|
|
62
|
+
auto_recall_available?: boolean;
|
|
63
|
+
auto_capture_available?: boolean;
|
|
64
|
+
min_sync_interval_seconds?: number;
|
|
65
|
+
autoRecallAvailable?: boolean;
|
|
66
|
+
autoCaptureAvailable?: boolean;
|
|
67
|
+
minSyncIntervalSeconds?: number;
|
|
65
68
|
};
|
|
66
69
|
}
|
|
67
70
|
|
|
@@ -129,5 +132,5 @@ export interface FileTrackingEntry {
|
|
|
129
132
|
|
|
130
133
|
// OpenClaw Plugin API — typed as `any` at boundaries.
|
|
131
134
|
// The real type comes from openclaw/plugin-sdk/core at runtime.
|
|
132
|
-
// Methods used: api.registerTool(), api.logger, api.
|
|
135
|
+
// Methods used: api.registerTool(), api.registerHook(), api.logger, api.pluginConfig
|
|
133
136
|
export type OpenClawPluginAPI = any;
|