@vectorize-io/hindsight-openclaw 0.5.0 → 0.6.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/dist/setup.js ADDED
@@ -0,0 +1,425 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Setup wizard for the Hindsight OpenClaw plugin.
4
+ *
5
+ * Two modes of operation:
6
+ *
7
+ * 1. Interactive — no flags, just run `hindsight-openclaw-setup`. Walks the
8
+ * user through picking a mode (Cloud / External API / Embedded daemon)
9
+ * via @clack/prompts and writes openclaw.json.
10
+ *
11
+ * 2. Non-interactive — pass `--mode cloud|api|embedded` plus the relevant
12
+ * flags for that mode. No prompts, intended for CI and scripted installs.
13
+ *
14
+ * Scanner-safe: does not import subprocess APIs and does not read environment
15
+ * variables directly. Pure config manipulation lives in setup-lib.ts.
16
+ */
17
+ import * as p from '@clack/prompts';
18
+ import { realpathSync } from 'fs';
19
+ import { resolve } from 'path';
20
+ import { fileURLToPath } from 'url';
21
+ import { DEFAULT_OPENCLAW_CONFIG_PATH, HINDSIGHT_CLOUD_URL, NO_KEY_PROVIDERS, applyApiMode, applyCloudMode, applyEmbeddedMode, defaultApiKeyEnvVar, ensurePluginConfig, isValidEnvVarName, loadConfig, saveConfig, summarizeApi, summarizeCloud, summarizeEmbedded, } from './setup-lib.js';
22
+ function usage() {
23
+ return [
24
+ 'Usage: hindsight-openclaw-setup [options] [config-path]',
25
+ '',
26
+ 'Interactive mode (no flags): walks through a TUI picker for Cloud /',
27
+ 'External API / Embedded daemon and writes the resulting plugin config',
28
+ `to ${DEFAULT_OPENCLAW_CONFIG_PATH} (or the positional config-path arg).`,
29
+ '',
30
+ 'Non-interactive mode: pass --mode and the relevant flags to skip the',
31
+ 'TUI. Suitable for CI and scripted setups.',
32
+ '',
33
+ 'Options:',
34
+ ' --config-path <path> Path to openclaw.json (default: ~/.openclaw/openclaw.json)',
35
+ ' --mode <mode> cloud | api | embedded (enables non-interactive mode)',
36
+ '',
37
+ 'Cloud mode:',
38
+ ` --api-url <url> Override the Hindsight Cloud URL (default: ${HINDSIGHT_CLOUD_URL})`,
39
+ ' --token-env <VAR> Env var holding the cloud API token (required)',
40
+ '',
41
+ 'External API mode:',
42
+ ' --api-url <url> Hindsight API URL (required)',
43
+ ' --token-env <VAR> Env var holding the API token (optional)',
44
+ ' --no-token Explicitly disable token auth',
45
+ '',
46
+ 'Embedded mode:',
47
+ ` --provider <id> LLM provider: ${['openai', 'anthropic', 'gemini', 'groq', ...NO_KEY_PROVIDERS].join(' | ')}`,
48
+ ' --api-key-env <VAR> Env var holding the LLM API key (required unless provider needs no key)',
49
+ ' --model <id> Optional model override (otherwise uses the provider default)',
50
+ '',
51
+ ' -h, --help Show this help',
52
+ '',
53
+ 'Examples:',
54
+ ' hindsight-openclaw-setup',
55
+ ' hindsight-openclaw-setup --mode cloud --token-env HINDSIGHT_CLOUD_TOKEN',
56
+ ' hindsight-openclaw-setup --mode api --api-url https://mcp.hindsight.example.com --no-token',
57
+ ' hindsight-openclaw-setup --mode embedded --provider openai --api-key-env OPENAI_API_KEY',
58
+ ' hindsight-openclaw-setup --mode embedded --provider claude-code',
59
+ ].join('\n');
60
+ }
61
+ export function parseCliArgs(argv) {
62
+ const args = { help: false, noToken: false };
63
+ for (let i = 0; i < argv.length; i++) {
64
+ const arg = argv[i];
65
+ const next = () => {
66
+ const value = argv[++i];
67
+ if (value === undefined) {
68
+ throw new Error(`missing value for ${arg}`);
69
+ }
70
+ return value;
71
+ };
72
+ switch (arg) {
73
+ case '-h':
74
+ case '--help':
75
+ args.help = true;
76
+ break;
77
+ case '--config-path':
78
+ args.configPath = next();
79
+ break;
80
+ case '--mode': {
81
+ const value = next();
82
+ if (value !== 'cloud' && value !== 'api' && value !== 'embedded') {
83
+ throw new Error(`invalid --mode: ${value} (expected cloud | api | embedded)`);
84
+ }
85
+ args.mode = value;
86
+ break;
87
+ }
88
+ case '--api-url':
89
+ args.apiUrl = next();
90
+ break;
91
+ case '--token-env':
92
+ args.tokenEnv = next();
93
+ break;
94
+ case '--no-token':
95
+ args.noToken = true;
96
+ break;
97
+ case '--provider':
98
+ args.provider = next();
99
+ break;
100
+ case '--api-key-env':
101
+ args.apiKeyEnv = next();
102
+ break;
103
+ case '--model':
104
+ args.model = next();
105
+ break;
106
+ default:
107
+ if (arg.startsWith('-')) {
108
+ throw new Error(`unknown argument: ${arg}`);
109
+ }
110
+ if (args.positional) {
111
+ throw new Error(`unexpected extra positional argument: ${arg}`);
112
+ }
113
+ args.positional = arg;
114
+ }
115
+ }
116
+ return args;
117
+ }
118
+ // ---------------------------------------------------------------------------
119
+ // Non-interactive execution
120
+ // ---------------------------------------------------------------------------
121
+ function buildCloudInput(args) {
122
+ if (!args.tokenEnv) {
123
+ throw new Error('--mode cloud requires --token-env <VAR>');
124
+ }
125
+ if (!isValidEnvVarName(args.tokenEnv)) {
126
+ throw new Error(`--token-env must be an UPPER_SNAKE_CASE env var name, got: ${args.tokenEnv}`);
127
+ }
128
+ return {
129
+ apiUrl: args.apiUrl,
130
+ tokenEnvVar: args.tokenEnv,
131
+ };
132
+ }
133
+ function buildApiInput(args) {
134
+ if (!args.apiUrl) {
135
+ throw new Error('--mode api requires --api-url <url>');
136
+ }
137
+ if (args.tokenEnv && args.noToken) {
138
+ throw new Error('--token-env and --no-token cannot both be set');
139
+ }
140
+ if (args.tokenEnv && !isValidEnvVarName(args.tokenEnv)) {
141
+ throw new Error(`--token-env must be an UPPER_SNAKE_CASE env var name, got: ${args.tokenEnv}`);
142
+ }
143
+ return {
144
+ apiUrl: args.apiUrl,
145
+ tokenEnvVar: args.tokenEnv,
146
+ };
147
+ }
148
+ function buildEmbeddedInput(args) {
149
+ if (!args.provider) {
150
+ throw new Error('--mode embedded requires --provider <id>');
151
+ }
152
+ const needsKey = !NO_KEY_PROVIDERS.has(args.provider);
153
+ if (needsKey) {
154
+ if (!args.apiKeyEnv) {
155
+ throw new Error(`--provider ${args.provider} requires --api-key-env <VAR> (providers that need no key: ${[...NO_KEY_PROVIDERS].join(', ')})`);
156
+ }
157
+ if (!isValidEnvVarName(args.apiKeyEnv)) {
158
+ throw new Error(`--api-key-env must be an UPPER_SNAKE_CASE env var name, got: ${args.apiKeyEnv}`);
159
+ }
160
+ }
161
+ return {
162
+ llmProvider: args.provider,
163
+ apiKeyEnvVar: args.apiKeyEnv,
164
+ llmModel: args.model,
165
+ };
166
+ }
167
+ export async function runNonInteractive(args, configPath) {
168
+ if (!args.mode) {
169
+ throw new Error('runNonInteractive called without --mode');
170
+ }
171
+ const cfg = await loadConfig(configPath);
172
+ const pluginConfig = ensurePluginConfig(cfg);
173
+ let summary;
174
+ if (args.mode === 'cloud') {
175
+ const input = buildCloudInput(args);
176
+ applyCloudMode(pluginConfig, input);
177
+ summary = summarizeCloud(input);
178
+ }
179
+ else if (args.mode === 'api') {
180
+ const input = buildApiInput(args);
181
+ applyApiMode(pluginConfig, input);
182
+ summary = summarizeApi(input);
183
+ }
184
+ else {
185
+ const input = buildEmbeddedInput(args);
186
+ applyEmbeddedMode(pluginConfig, input);
187
+ summary = summarizeEmbedded(input);
188
+ }
189
+ await saveConfig(configPath, cfg);
190
+ return { summary, configPath };
191
+ }
192
+ // ---------------------------------------------------------------------------
193
+ // Interactive (TUI) execution
194
+ // ---------------------------------------------------------------------------
195
+ const validateEnvVar = (value) => isValidEnvVarName(value) ? undefined : 'Must be an UPPER_SNAKE_CASE env var name';
196
+ const validateRequired = (msg) => (value) => value && value.trim().length > 0 ? undefined : msg;
197
+ function assertNotCancelled(value) {
198
+ if (p.isCancel(value)) {
199
+ p.cancel('Setup cancelled.');
200
+ process.exit(1);
201
+ }
202
+ }
203
+ async function promptCloud(pluginConfig) {
204
+ const useDefaultUrl = await p.confirm({
205
+ message: `Use the default Hindsight Cloud URL (${HINDSIGHT_CLOUD_URL})?`,
206
+ initialValue: true,
207
+ });
208
+ assertNotCancelled(useDefaultUrl);
209
+ let apiUrl;
210
+ if (!useDefaultUrl) {
211
+ const custom = await p.text({
212
+ message: 'Hindsight Cloud URL',
213
+ placeholder: HINDSIGHT_CLOUD_URL,
214
+ validate: validateRequired('URL is required'),
215
+ });
216
+ assertNotCancelled(custom);
217
+ apiUrl = custom;
218
+ }
219
+ const tokenEnvVar = await p.text({
220
+ message: 'Environment variable holding your Hindsight Cloud API token',
221
+ placeholder: 'HINDSIGHT_CLOUD_TOKEN',
222
+ initialValue: 'HINDSIGHT_CLOUD_TOKEN',
223
+ validate: validateEnvVar,
224
+ });
225
+ assertNotCancelled(tokenEnvVar);
226
+ const input = { apiUrl, tokenEnvVar };
227
+ applyCloudMode(pluginConfig, input);
228
+ return summarizeCloud(input);
229
+ }
230
+ async function promptApi(pluginConfig) {
231
+ const apiUrl = await p.text({
232
+ message: 'Hindsight API URL',
233
+ placeholder: 'https://mcp.hindsight.example.com',
234
+ validate: validateRequired('URL is required'),
235
+ });
236
+ assertNotCancelled(apiUrl);
237
+ const needsToken = await p.confirm({
238
+ message: 'Does this API require an auth token?',
239
+ initialValue: false,
240
+ });
241
+ assertNotCancelled(needsToken);
242
+ let tokenEnvVar;
243
+ if (needsToken) {
244
+ const value = await p.text({
245
+ message: 'Environment variable holding the API token',
246
+ placeholder: 'HINDSIGHT_API_TOKEN',
247
+ initialValue: 'HINDSIGHT_API_TOKEN',
248
+ validate: validateEnvVar,
249
+ });
250
+ assertNotCancelled(value);
251
+ tokenEnvVar = value;
252
+ }
253
+ const input = { apiUrl, tokenEnvVar };
254
+ applyApiMode(pluginConfig, input);
255
+ return summarizeApi(input);
256
+ }
257
+ async function promptEmbedded(pluginConfig) {
258
+ const provider = await p.select({
259
+ message: 'LLM provider used by the Hindsight memory daemon',
260
+ options: [
261
+ { value: 'openai', label: 'OpenAI', hint: 'API key required' },
262
+ { value: 'anthropic', label: 'Anthropic', hint: 'API key required' },
263
+ { value: 'gemini', label: 'Gemini', hint: 'API key required' },
264
+ { value: 'groq', label: 'Groq', hint: 'API key required' },
265
+ {
266
+ value: 'claude-code',
267
+ label: 'Claude Code',
268
+ hint: 'no API key needed (uses Claude Code CLI auth)',
269
+ },
270
+ {
271
+ value: 'openai-codex',
272
+ label: 'OpenAI Codex',
273
+ hint: 'no API key needed (uses codex auth login)',
274
+ },
275
+ { value: 'ollama', label: 'Ollama', hint: 'no API key needed (local models)' },
276
+ ],
277
+ });
278
+ assertNotCancelled(provider);
279
+ const llmProvider = provider;
280
+ let apiKeyEnvVar;
281
+ if (!NO_KEY_PROVIDERS.has(llmProvider)) {
282
+ const defaultEnvId = defaultApiKeyEnvVar(llmProvider);
283
+ const envId = await p.text({
284
+ message: `Environment variable holding your ${llmProvider} API key`,
285
+ placeholder: defaultEnvId,
286
+ initialValue: defaultEnvId,
287
+ validate: validateEnvVar,
288
+ });
289
+ assertNotCancelled(envId);
290
+ apiKeyEnvVar = envId;
291
+ }
292
+ const overrideModel = await p.confirm({
293
+ message: 'Override the default model?',
294
+ initialValue: false,
295
+ });
296
+ assertNotCancelled(overrideModel);
297
+ let llmModel;
298
+ if (overrideModel) {
299
+ const value = await p.text({
300
+ message: 'Model id',
301
+ placeholder: 'gpt-4o-mini',
302
+ validate: validateRequired('Model id is required'),
303
+ });
304
+ assertNotCancelled(value);
305
+ llmModel = value;
306
+ }
307
+ const input = { llmProvider, apiKeyEnvVar, llmModel };
308
+ applyEmbeddedMode(pluginConfig, input);
309
+ return summarizeEmbedded(input);
310
+ }
311
+ async function runInteractive(configPath) {
312
+ p.intro('🦞 Hindsight Memory setup for OpenClaw');
313
+ p.log.info(`Config file: ${configPath}`);
314
+ const cfg = await loadConfig(configPath);
315
+ const pluginConfig = ensurePluginConfig(cfg);
316
+ const mode = await p.select({
317
+ message: 'How do you want to run Hindsight?',
318
+ options: [
319
+ { value: 'cloud', label: 'Cloud', hint: 'managed Hindsight, no local setup' },
320
+ { value: 'api', label: 'External API', hint: 'your own running Hindsight deployment' },
321
+ {
322
+ value: 'embedded',
323
+ label: 'Embedded daemon',
324
+ hint: 'spawn a local hindsight daemon on this machine',
325
+ },
326
+ ],
327
+ });
328
+ assertNotCancelled(mode);
329
+ let summary;
330
+ if (mode === 'cloud') {
331
+ summary = await promptCloud(pluginConfig);
332
+ }
333
+ else if (mode === 'api') {
334
+ summary = await promptApi(pluginConfig);
335
+ }
336
+ else {
337
+ summary = await promptEmbedded(pluginConfig);
338
+ }
339
+ const spin = p.spinner();
340
+ spin.start('Writing configuration');
341
+ await saveConfig(configPath, cfg);
342
+ spin.stop(`Saved to ${configPath}`);
343
+ p.note([
344
+ summary,
345
+ '',
346
+ 'Next steps:',
347
+ ' 1. Ensure any referenced env vars are exported in the shell that runs the gateway.',
348
+ ' 2. Restart the gateway: openclaw gateway restart',
349
+ ' 3. Verify config: openclaw config validate',
350
+ ].join('\n'), 'Hindsight Memory configured');
351
+ p.outro('Done.');
352
+ return { summary, configPath };
353
+ }
354
+ // ---------------------------------------------------------------------------
355
+ // Main
356
+ // ---------------------------------------------------------------------------
357
+ async function main() {
358
+ let args;
359
+ try {
360
+ args = parseCliArgs(process.argv.slice(2));
361
+ }
362
+ catch (err) {
363
+ console.error(`hindsight-openclaw-setup: ${err instanceof Error ? err.message : err}`);
364
+ console.error();
365
+ console.error(usage());
366
+ process.exit(2);
367
+ }
368
+ if (args.help) {
369
+ console.log(usage());
370
+ return;
371
+ }
372
+ const configPath = args.configPath ?? args.positional ?? DEFAULT_OPENCLAW_CONFIG_PATH;
373
+ if (args.mode) {
374
+ // Non-interactive path for scripts and CI.
375
+ try {
376
+ const { summary } = await runNonInteractive(args, configPath);
377
+ console.log(`Hindsight Memory configured: ${summary}`);
378
+ console.log(`Saved to ${configPath}`);
379
+ }
380
+ catch (err) {
381
+ console.error(`hindsight-openclaw-setup: ${err instanceof Error ? err.message : err}`);
382
+ process.exit(1);
383
+ }
384
+ return;
385
+ }
386
+ // Interactive path (default).
387
+ await runInteractive(configPath);
388
+ }
389
+ /**
390
+ * Only run `main()` when this file is the Node entry point. Importing it from
391
+ * a test (or any other module) should not trigger the interactive wizard.
392
+ *
393
+ * When invoked through `node_modules/.bin/hindsight-openclaw-setup` (npm-created
394
+ * symlink), `process.argv[1]` points at the symlink while `import.meta.url`
395
+ * resolves to the real file. Canonicalize both via `realpath` so the check
396
+ * still matches — otherwise `main()` never runs on bin invocations and the
397
+ * command silently exits with no output.
398
+ */
399
+ function canonicalize(path) {
400
+ const resolved = resolve(path);
401
+ try {
402
+ return realpathSync(resolved);
403
+ }
404
+ catch {
405
+ return resolved;
406
+ }
407
+ }
408
+ function isDirectRun() {
409
+ const entry = process.argv[1];
410
+ if (!entry)
411
+ return false;
412
+ try {
413
+ return canonicalize(entry) === canonicalize(fileURLToPath(import.meta.url));
414
+ }
415
+ catch {
416
+ return false;
417
+ }
418
+ }
419
+ if (isDirectRun()) {
420
+ main().catch((err) => {
421
+ const msg = err instanceof Error ? err.message : String(err);
422
+ console.error(`hindsight-openclaw-setup failed: ${msg}`);
423
+ process.exit(1);
424
+ });
425
+ }
package/dist/types.d.ts CHANGED
@@ -48,12 +48,16 @@ export interface PluginConfig {
48
48
  embedPackagePath?: string;
49
49
  llmProvider?: string;
50
50
  llmModel?: string;
51
- llmApiKeyEnv?: string;
51
+ llmApiKey?: string;
52
+ llmBaseUrl?: string;
52
53
  apiPort?: number;
53
54
  hindsightApiUrl?: string;
54
55
  hindsightApiToken?: string;
55
56
  dynamicBankId?: boolean;
57
+ bankId?: string;
56
58
  bankIdPrefix?: string;
59
+ retainTags?: string[];
60
+ retainSource?: string;
57
61
  excludeProviders?: string[];
58
62
  autoRecall?: boolean;
59
63
  dynamicBankGranularity?: Array<'agent' | 'provider' | 'channel' | 'user'>;
@@ -71,57 +75,53 @@ export interface PluginConfig {
71
75
  recallMaxQueryChars?: number;
72
76
  recallPromptPreamble?: string;
73
77
  recallInjectionPosition?: 'prepend' | 'append' | 'user';
78
+ ignoreSessionPatterns?: string[];
79
+ statelessSessionPatterns?: string[];
80
+ skipStatelessSessions?: boolean;
74
81
  debug?: boolean;
75
82
  logLevel?: 'off' | 'error' | 'warning' | 'info' | 'debug';
76
83
  logSummaryIntervalMs?: number;
84
+ retainQueuePath?: string;
85
+ retainQueueMaxAgeMs?: number;
86
+ retainQueueFlushIntervalMs?: number;
77
87
  }
78
88
  export interface ServiceConfig {
79
89
  id: string;
80
90
  start(): Promise<void>;
81
91
  stop(): Promise<void>;
82
92
  }
93
+ export type { RecallResult as MemoryResult, RecallResponse, ReflectResponse } from '@vectorize-io/hindsight-client';
94
+ /**
95
+ * Internal retain payload shape built by `buildRetainRequest`. Not a
96
+ * re-export from the generated client — the generated client's retain()
97
+ * takes bankId + content + options as positional args, whereas we build up a
98
+ * single object inside the plugin and translate it at the call site. Keeping
99
+ * this type local means tests can assert the shape without pulling in
100
+ * generated types.
101
+ */
83
102
  export interface RetainRequest {
84
103
  content: string;
85
- document_id?: string;
104
+ documentId?: string;
86
105
  metadata?: Record<string, unknown>;
106
+ tags?: string[];
87
107
  }
88
- export interface RetainResponse {
89
- message: string;
90
- document_id: string;
91
- memory_unit_ids: string[];
92
- }
93
- export interface RecallRequest {
94
- query: string;
95
- max_tokens?: number;
96
- budget?: 'low' | 'mid' | 'high';
97
- types?: Array<'world' | 'experience' | 'observation'>;
98
- }
99
- export interface RecallResponse {
100
- results: MemoryResult[];
101
- entities: Record<string, unknown> | null;
102
- trace: unknown | null;
103
- chunks: unknown | null;
104
- }
105
- export interface MemoryResult {
106
- id: string;
107
- text: string;
108
- type: string;
109
- entities: string[];
110
- context: string;
111
- occurred_start: string | null;
112
- occurred_end: string | null;
113
- mentioned_at: string | null;
114
- document_id: string | null;
115
- metadata: Record<string, unknown> | null;
116
- chunk_id: string | null;
117
- tags: string[];
118
- }
119
- export interface CreateBankRequest {
120
- name: string;
121
- background_context?: string;
122
- }
123
- export interface CreateBankResponse {
108
+ /**
109
+ * Stats returned by `GET /v1/default/banks/{bank_id}/stats`. The generated
110
+ * high-level client does not expose this endpoint yet; backfill calls it
111
+ * directly via `fetch`.
112
+ */
113
+ export interface BankStats {
124
114
  bank_id: string;
125
- name: string;
126
- created_at: string;
115
+ total_nodes: number;
116
+ total_links: number;
117
+ total_documents: number;
118
+ pending_operations: number;
119
+ failed_operations: number;
120
+ pending_consolidation: number;
121
+ last_consolidated_at: string | null;
122
+ total_observations: number;
123
+ nodes_by_fact_type?: Record<string, number>;
124
+ links_by_link_type?: Record<string, number>;
125
+ links_by_fact_type?: Record<string, number>;
126
+ links_breakdown?: Record<string, unknown>;
127
127
  }