@hippodid/openclaw-plugin 1.0.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/LICENSE +190 -0
- package/README.md +141 -0
- package/openclaw.plugin.json +57 -0
- package/package.json +38 -0
- package/src/file-sync.ts +250 -0
- package/src/hippodid-client.ts +283 -0
- package/src/hooks/auto-capture.ts +58 -0
- package/src/hooks/auto-recall.ts +67 -0
- package/src/hooks/memory-flush.ts +22 -0
- package/src/hooks/session-lifecycle.ts +42 -0
- package/src/index.ts +220 -0
- package/src/tier-manager.ts +90 -0
- package/src/types.ts +160 -0
- package/src/workspace-detector.ts +104 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import type { HippoDidClient } from './hippodid-client.js';
|
|
2
|
+
import type { TierInfo } from './types.js';
|
|
3
|
+
|
|
4
|
+
export interface TierManager {
|
|
5
|
+
initialize(): Promise<TierInfo>;
|
|
6
|
+
getCurrentTier(): TierInfo;
|
|
7
|
+
shouldMountFileSync(autoCaptureEnabled: boolean): boolean;
|
|
8
|
+
shouldMountAutoRecall(autoRecallEnabled: boolean): boolean;
|
|
9
|
+
shouldMountAutoCapture(autoCaptureEnabled: boolean): boolean;
|
|
10
|
+
shouldHydrateOnStart(autoRecallEnabled: boolean): boolean;
|
|
11
|
+
getEffectiveSyncInterval(configInterval: number): number;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const FREE_TIER_FALLBACK: TierInfo = {
|
|
15
|
+
tier: 'free',
|
|
16
|
+
features: {
|
|
17
|
+
autoRecallAvailable: false,
|
|
18
|
+
autoCaptureAvailable: false,
|
|
19
|
+
minSyncIntervalSeconds: 60,
|
|
20
|
+
},
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function createTierManager(
|
|
24
|
+
client: HippoDidClient,
|
|
25
|
+
characterId: string,
|
|
26
|
+
logger: { info(msg: string): void; warn(msg: string): void },
|
|
27
|
+
): TierManager {
|
|
28
|
+
let currentTier: TierInfo = FREE_TIER_FALLBACK;
|
|
29
|
+
|
|
30
|
+
return {
|
|
31
|
+
async initialize(): Promise<TierInfo> {
|
|
32
|
+
const result = await client.getTier(characterId);
|
|
33
|
+
|
|
34
|
+
if (result.ok) {
|
|
35
|
+
currentTier = result.value;
|
|
36
|
+
} else {
|
|
37
|
+
logger.warn(
|
|
38
|
+
`hippodid: failed to fetch tier, defaulting to free: ${result.error.message}`,
|
|
39
|
+
);
|
|
40
|
+
currentTier = FREE_TIER_FALLBACK;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
logger.info(
|
|
44
|
+
`hippodid: tier=${currentTier.tier}, autoRecall=${currentTier.features.autoRecallAvailable ? 'available' : 'unavailable'}, autoCapture=${currentTier.features.autoCaptureAvailable ? 'available' : 'unavailable'}`,
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return currentTier;
|
|
48
|
+
},
|
|
49
|
+
|
|
50
|
+
getCurrentTier(): TierInfo {
|
|
51
|
+
return currentTier;
|
|
52
|
+
},
|
|
53
|
+
|
|
54
|
+
shouldMountFileSync(autoCaptureEnabled: boolean): boolean {
|
|
55
|
+
const isFree = !isPaidTier(currentTier);
|
|
56
|
+
if (isFree) return true;
|
|
57
|
+
return !autoCaptureEnabled;
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
shouldMountAutoRecall(autoRecallEnabled: boolean): boolean {
|
|
61
|
+
return (
|
|
62
|
+
isPaidTier(currentTier) &&
|
|
63
|
+
autoRecallEnabled &&
|
|
64
|
+
currentTier.features.autoRecallAvailable
|
|
65
|
+
);
|
|
66
|
+
},
|
|
67
|
+
|
|
68
|
+
shouldMountAutoCapture(autoCaptureEnabled: boolean): boolean {
|
|
69
|
+
return (
|
|
70
|
+
isPaidTier(currentTier) &&
|
|
71
|
+
autoCaptureEnabled &&
|
|
72
|
+
currentTier.features.autoCaptureAvailable
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
shouldHydrateOnStart(autoRecallEnabled: boolean): boolean {
|
|
77
|
+
const isFree = !isPaidTier(currentTier);
|
|
78
|
+
if (isFree) return true;
|
|
79
|
+
return !autoRecallEnabled;
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
getEffectiveSyncInterval(configInterval: number): number {
|
|
83
|
+
return Math.max(configInterval, currentTier.features.minSyncIntervalSeconds);
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isPaidTier(tier: TierInfo): boolean {
|
|
89
|
+
return tier.tier !== 'free';
|
|
90
|
+
}
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// --- Result type (discriminated union, zero deps) ---
|
|
2
|
+
|
|
3
|
+
export type Result<T, E = ApiError> =
|
|
4
|
+
| { ok: true; value: T }
|
|
5
|
+
| { ok: false; error: E };
|
|
6
|
+
|
|
7
|
+
export function ok<T>(value: T): Result<T, never> {
|
|
8
|
+
return { ok: true, value };
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function err<E>(error: E): Result<never, E> {
|
|
12
|
+
return { ok: false, error };
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// --- API error ---
|
|
16
|
+
|
|
17
|
+
export interface ApiError {
|
|
18
|
+
status: number;
|
|
19
|
+
message: string;
|
|
20
|
+
retryable: boolean;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// --- Plugin config (matches openclaw.plugin.json configSchema) ---
|
|
24
|
+
|
|
25
|
+
export interface PluginConfig {
|
|
26
|
+
apiKey: string;
|
|
27
|
+
characterId: string;
|
|
28
|
+
baseUrl: string;
|
|
29
|
+
syncIntervalSeconds: number;
|
|
30
|
+
autoRecall: boolean;
|
|
31
|
+
autoCapture: boolean;
|
|
32
|
+
additionalPaths: WatchPathConfig[];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface WatchPathConfig {
|
|
36
|
+
path: string;
|
|
37
|
+
label?: string;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Watch path (resolved) ---
|
|
41
|
+
|
|
42
|
+
export interface WatchPath {
|
|
43
|
+
path: string;
|
|
44
|
+
label: string;
|
|
45
|
+
source: 'auto-detected' | 'user-specified';
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// --- Tier info ---
|
|
49
|
+
|
|
50
|
+
export interface TierInfo {
|
|
51
|
+
tier: string;
|
|
52
|
+
features: {
|
|
53
|
+
autoRecallAvailable: boolean;
|
|
54
|
+
autoCaptureAvailable: boolean;
|
|
55
|
+
minSyncIntervalSeconds: number;
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface TierApiResponse {
|
|
60
|
+
tier: string;
|
|
61
|
+
features: {
|
|
62
|
+
auto_recall_available: boolean;
|
|
63
|
+
auto_capture_available: boolean;
|
|
64
|
+
min_sync_interval_seconds: number;
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// --- Sync responses ---
|
|
69
|
+
|
|
70
|
+
export interface SyncResponse {
|
|
71
|
+
status: string;
|
|
72
|
+
snapshotId: string;
|
|
73
|
+
changed: boolean;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
export interface SyncApiResponse {
|
|
77
|
+
status: string;
|
|
78
|
+
snapshot_id: string;
|
|
79
|
+
changed: boolean;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface SyncLatestResponse {
|
|
83
|
+
sourcePath: string;
|
|
84
|
+
fileContent: string;
|
|
85
|
+
snapshotId: string;
|
|
86
|
+
syncedAt: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SyncLatestApiResponse {
|
|
90
|
+
source_path: string;
|
|
91
|
+
file_content: string;
|
|
92
|
+
snapshot_id: string;
|
|
93
|
+
synced_at: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export interface SyncStatusEntry {
|
|
97
|
+
sourcePath: string;
|
|
98
|
+
label: string;
|
|
99
|
+
lastSyncedAt: string;
|
|
100
|
+
snapshotId: string;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export interface SyncStatusResponse {
|
|
104
|
+
entries: SyncStatusEntry[];
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- Search ---
|
|
108
|
+
|
|
109
|
+
export interface SearchResult {
|
|
110
|
+
content: string;
|
|
111
|
+
category: string;
|
|
112
|
+
score: number;
|
|
113
|
+
createdAt: string;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
export interface SearchResultApiResponse {
|
|
117
|
+
content: string;
|
|
118
|
+
category: string;
|
|
119
|
+
score: number;
|
|
120
|
+
created_at: string;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// --- File tracking ---
|
|
124
|
+
|
|
125
|
+
export interface FileTrackingEntry {
|
|
126
|
+
hash: string;
|
|
127
|
+
lastSyncedAt: Date;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- OpenClaw Plugin API (local type, based on plan assumptions) ---
|
|
131
|
+
|
|
132
|
+
export interface OpenClawPluginAPI {
|
|
133
|
+
config: PluginConfig;
|
|
134
|
+
logger: {
|
|
135
|
+
info(message: string): void;
|
|
136
|
+
warn(message: string): void;
|
|
137
|
+
error(message: string): void;
|
|
138
|
+
};
|
|
139
|
+
hooks: {
|
|
140
|
+
on(event: string, handler: (...args: any[]) => void | Promise<void>): void;
|
|
141
|
+
};
|
|
142
|
+
context: {
|
|
143
|
+
prepend(content: string): void;
|
|
144
|
+
};
|
|
145
|
+
commands: {
|
|
146
|
+
register(name: string, options: CommandOptions): void;
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface CommandOptions {
|
|
151
|
+
description: string;
|
|
152
|
+
args?: CommandArg[];
|
|
153
|
+
handler: (args: Record<string, string>) => void | Promise<void>;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface CommandArg {
|
|
157
|
+
name: string;
|
|
158
|
+
description: string;
|
|
159
|
+
required?: boolean;
|
|
160
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
2
|
+
import { resolve, join } from 'node:path';
|
|
3
|
+
import { homedir } from 'node:os';
|
|
4
|
+
import type { PluginConfig, WatchPath } from './types.js';
|
|
5
|
+
|
|
6
|
+
export function detectWorkspaceMemoryDir(): string | null {
|
|
7
|
+
const candidates: string[] = [];
|
|
8
|
+
|
|
9
|
+
const envWorkspace = process.env['OPENCLAW_WORKSPACE'];
|
|
10
|
+
if (envWorkspace) {
|
|
11
|
+
candidates.push(resolve(join(envWorkspace, 'memory')));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
candidates.push(resolve(join(homedir(), '.openclaw', 'workspace', 'memory')));
|
|
15
|
+
candidates.push(resolve(join(process.cwd(), 'memory')));
|
|
16
|
+
|
|
17
|
+
for (const candidate of candidates) {
|
|
18
|
+
if (isValidMemoryDir(candidate)) {
|
|
19
|
+
return candidate;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function detectMemoryMdPath(): string | null {
|
|
27
|
+
const candidates: string[] = [];
|
|
28
|
+
|
|
29
|
+
const envWorkspace = process.env['OPENCLAW_WORKSPACE'];
|
|
30
|
+
if (envWorkspace) {
|
|
31
|
+
candidates.push(resolve(join(envWorkspace, 'MEMORY.md')));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
candidates.push(
|
|
35
|
+
resolve(join(homedir(), '.openclaw', 'workspace', 'MEMORY.md')),
|
|
36
|
+
);
|
|
37
|
+
candidates.push(resolve(join(process.cwd(), 'MEMORY.md')));
|
|
38
|
+
|
|
39
|
+
for (const candidate of candidates) {
|
|
40
|
+
if (existsSync(candidate)) {
|
|
41
|
+
return candidate;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveWatchPaths(config: PluginConfig): WatchPath[] {
|
|
49
|
+
const paths: WatchPath[] = [];
|
|
50
|
+
const seen = new Set<string>();
|
|
51
|
+
|
|
52
|
+
const memoryDir = detectWorkspaceMemoryDir();
|
|
53
|
+
if (memoryDir) {
|
|
54
|
+
const resolved = resolve(memoryDir);
|
|
55
|
+
if (!seen.has(resolved)) {
|
|
56
|
+
seen.add(resolved);
|
|
57
|
+
paths.push({
|
|
58
|
+
path: resolved,
|
|
59
|
+
label: 'workspace-memory',
|
|
60
|
+
source: 'auto-detected',
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const memoryMd = detectMemoryMdPath();
|
|
66
|
+
if (memoryMd) {
|
|
67
|
+
const resolved = resolve(memoryMd);
|
|
68
|
+
if (!seen.has(resolved)) {
|
|
69
|
+
seen.add(resolved);
|
|
70
|
+
paths.push({
|
|
71
|
+
path: resolved,
|
|
72
|
+
label: 'MEMORY.md',
|
|
73
|
+
source: 'auto-detected',
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for (const additional of config.additionalPaths) {
|
|
79
|
+
const resolved = resolve(additional.path);
|
|
80
|
+
if (!seen.has(resolved)) {
|
|
81
|
+
seen.add(resolved);
|
|
82
|
+
paths.push({
|
|
83
|
+
path: resolved,
|
|
84
|
+
label: additional.label ?? resolved,
|
|
85
|
+
source: 'user-specified',
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return paths;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isValidMemoryDir(dirPath: string): boolean {
|
|
94
|
+
try {
|
|
95
|
+
if (!existsSync(dirPath)) return false;
|
|
96
|
+
const s = statSync(dirPath);
|
|
97
|
+
if (!s.isDirectory()) return false;
|
|
98
|
+
|
|
99
|
+
const entries = readdirSync(dirPath);
|
|
100
|
+
return entries.some((e) => e.endsWith('.md'));
|
|
101
|
+
} catch {
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
}
|