@hippodid/openclaw-plugin 1.0.9 → 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/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 VERSION = '1.0.0';
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
- const config = resolveConfig(api.pluginConfig ?? {});
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?.apiKey?.trim() ?? '';
28
- const characterId = config?.characterId?.trim() ?? '';
29
-
35
+ const apiKey = config.apiKey.trim();
36
+ const characterId = config.characterId.trim();
30
37
  if (!apiKey || !characterId) {
31
- logger.warn('HippoDid: apiKey and characterId required — configure in openclaw.json');
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,35 +157,145 @@ function registerCommands(
152
157
  tierManager: TierManager,
153
158
  logger: { info(msg: string): void; warn(msg: string): void },
154
159
  ): void {
155
- api.registerTool({
156
- name: 'hippodid:status',
157
- description: 'Show HippoDid tier, sync status, and watched paths',
158
- execute: async () => {
159
- const tier = tierManager.getCurrentTier();
160
- const statusResult = await client.getSyncStatus(config.characterId);
161
-
162
- logger.info(`--- HippoDid Status ---`);
163
- logger.info(`Character: ${config.characterId}`);
164
- logger.info(`Tier: ${tier.tier}`);
165
- logger.info(
166
- `Auto-Recall: ${tier.features.autoRecallAvailable ? 'available' : 'unavailable'} (config: ${config.autoRecall ? 'ON' : 'OFF'})`,
167
- );
168
- logger.info(
169
- `Auto-Capture: ${tier.features.autoCaptureAvailable ? 'available' : 'unavailable'} (config: ${config.autoCapture ? 'ON' : 'OFF'})`,
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})`,
170
181
  );
182
+ }
171
183
 
172
- if (statusResult.ok) {
173
- logger.info(`Synced sources: ${statusResult.value.entries.length}`);
174
- for (const entry of statusResult.value.entries) {
175
- logger.info(
176
- ` ${entry.sourcePath} (${entry.label}) last sync: ${entry.lastSyncedAt}`,
177
- );
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
+ });
178
208
  }
179
- } else {
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) {
180
244
  logger.warn(
181
- `Could not fetch sync status: ${statusResult.error.message}`,
245
+ `hippodid: import failed for ${file.path}: ${e instanceof Error ? e.message : 'unknown'}`,
182
246
  );
183
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;
184
299
  },
185
300
  });
186
301
 
@@ -190,9 +305,9 @@ function registerCommands(
190
305
  execute: async () => {
191
306
  logger.info('hippodid: manual sync triggered...');
192
307
  const { synced, changed } = await fileSync.flushNow();
193
- logger.info(
194
- `hippodid: manual sync complete — ${synced} files (${changed} changed)`,
195
- );
308
+ const text = `hippodid: manual sync complete — ${synced} files (${changed} changed)`;
309
+ logger.info(text);
310
+ return text;
196
311
  },
197
312
  });
198
313
 
@@ -207,72 +322,9 @@ function registerCommands(
207
322
  },
208
323
  ],
209
324
  execute: async (args: Record<string, string>) => {
210
- const { readdir } = await import('node:fs/promises');
211
- const { join, extname } = await import('node:path');
212
- const { createHash } = await import('node:crypto');
213
-
214
- const workspacePath = args['workspace']
215
- ? resolve(args['workspace'])
216
- : resolve(process.cwd());
217
-
218
- const memoryDir = join(workspacePath, 'memory');
219
- const memoryMd = join(workspacePath, 'MEMORY.md');
220
- const filesToImport: Array<{ path: string; label: string }> = [];
221
-
222
- try {
223
- const entries = await readdir(memoryDir);
224
- for (const entry of entries) {
225
- if (extname(entry) === '.md') {
226
- filesToImport.push({
227
- path: join(memoryDir, entry),
228
- label: 'workspace-memory',
229
- });
230
- }
231
- }
232
- } catch {
233
- // memory dir may not exist
234
- }
235
-
236
- try {
237
- await readFile(memoryMd);
238
- filesToImport.push({ path: memoryMd, label: 'MEMORY.md' });
239
- } catch {
240
- // MEMORY.md may not exist
241
- }
242
-
243
- if (filesToImport.length === 0) {
244
- logger.info('hippodid: no memory files found to import');
245
- return;
246
- }
247
-
248
- logger.info(
249
- `hippodid: importing ${filesToImport.length} files from ${workspacePath}...`,
250
- );
251
-
252
- let imported = 0;
253
- for (const file of filesToImport) {
254
- try {
255
- const content = await readFile(file.path);
256
- const hash = createHash('sha256').update(content).digest('hex');
257
- const base64 = content.toString('base64');
258
- const result = await client.syncFile(
259
- config.characterId,
260
- file.path,
261
- file.label,
262
- base64,
263
- hash,
264
- );
265
- if (result.ok) imported++;
266
- } catch (e) {
267
- logger.warn(
268
- `hippodid: import failed for ${file.path}: ${e instanceof Error ? e.message : 'unknown'}`,
269
- );
270
- }
271
- }
272
-
273
- logger.info(
274
- `hippodid: import complete — ${imported}/${filesToImport.length} files imported`,
275
- );
325
+ const text = await runImport(args['workspace']);
326
+ logger.info(text);
327
+ return text;
276
328
  },
277
329
  });
278
330
  }
@@ -51,10 +51,8 @@ export function createTierManager(
51
51
  return currentTier;
52
52
  },
53
53
 
54
- shouldMountFileSync(autoCaptureEnabled: boolean): boolean {
55
- const isFree = !isPaidTier(currentTier);
56
- if (isFree) return true;
57
- return !autoCaptureEnabled;
54
+ shouldMountFileSync(_autoCaptureEnabled: boolean): boolean {
55
+ return true;
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: boolean;
63
- auto_capture_available: boolean;
64
- min_sync_interval_seconds: number;
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.config
135
+ // Methods used: api.registerTool(), api.registerHook(), api.logger, api.pluginConfig
133
136
  export type OpenClawPluginAPI = any;