diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index a4352280..302c1f4e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -150,6 +150,55 @@ jobs:
path: hindsight-clients/typescript/*.tgz
retention-days: 1
+ release-hindsight-all-npm:
+ runs-on: ubuntu-latest
+ environment: npm
+
+ steps:
+ - uses: actions/checkout@v6
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+ registry-url: 'https://registry.npmjs.org'
+ cache: 'npm'
+ cache-dependency-path: package-lock.json
+
+ - name: Install dependencies
+ run: npm ci --workspace=hindsight-all-npm
+
+ - name: Build
+ run: npm run build --workspace=hindsight-all-npm
+
+ - name: Publish to npm
+ working-directory: ./hindsight-all-npm
+ run: |
+ set +e
+ OUTPUT=$(npm publish --access public 2>&1)
+ EXIT_CODE=$?
+ echo "$OUTPUT"
+ if [ $EXIT_CODE -ne 0 ]; then
+ if echo "$OUTPUT" | grep -q "cannot publish over"; then
+ echo "Package version already published, skipping..."
+ exit 0
+ fi
+ exit $EXIT_CODE
+ fi
+ env:
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
+
+ - name: Pack for GitHub release
+ working-directory: ./hindsight-all-npm
+ run: npm pack
+
+ - name: Upload artifacts
+ uses: actions/upload-artifact@v7
+ with:
+ name: hindsight-all-npm
+ path: hindsight-all-npm/*.tgz
+ retention-days: 1
+
release-control-plane:
runs-on: ubuntu-latest
environment: npm
@@ -407,7 +456,7 @@ jobs:
create-github-release:
runs-on: ubuntu-latest
- needs: [release-python-packages, release-typescript-client, release-control-plane, release-rust-cli, release-docker-images, release-helm-chart]
+ needs: [release-python-packages, release-typescript-client, release-hindsight-all-npm, release-control-plane, release-rust-cli, release-docker-images, release-helm-chart]
permissions:
contents: write
@@ -436,6 +485,12 @@ jobs:
name: control-plane
path: ./artifacts/control-plane
+ - name: Download hindsight-embed npm wrapper
+ uses: actions/download-artifact@v8
+ with:
+ name: hindsight-all-npm
+ path: ./artifacts/hindsight-all-npm
+
- name: Download Rust CLI (Linux)
uses: actions/download-artifact@v8
with:
@@ -472,6 +527,8 @@ jobs:
cp artifacts/python-packages/hindsight-embed/dist/* release-assets/ || true
# TypeScript client
cp artifacts/typescript-client/*.tgz release-assets/ || true
+ # hindsight-embed npm wrapper
+ cp artifacts/hindsight-all-npm/*.tgz release-assets/ || true
# Control Plane
cp artifacts/control-plane/*.tgz release-assets/ || true
# Rust CLI binaries
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index ac31dccf..5b0c6e45 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -32,6 +32,7 @@ jobs:
helm: ${{ steps.filter.outputs.helm }}
docs: ${{ steps.filter.outputs.docs }}
embed: ${{ steps.filter.outputs.embed }}
+ all-npm: ${{ steps.filter.outputs.all-npm }}
hindsight-all: ${{ steps.filter.outputs.hindsight-all }}
integration-tests: ${{ steps.filter.outputs.integration-tests }}
integrations-openclaw: ${{ steps.filter.outputs.integrations-openclaw }}
@@ -92,6 +93,10 @@ jobs:
- '*.md'
embed:
- 'hindsight-embed/**'
+ all-npm:
+ - 'hindsight-all-npm/**'
+ - 'package.json'
+ - 'package-lock.json'
hindsight-all:
- 'hindsight-all/**'
integration-tests:
@@ -183,12 +188,12 @@ jobs:
- name: Build TypeScript client
run: npm run build --workspace=hindsight-clients/typescript
- build-openclaw-integration:
+ build-hindsight-all-npm:
needs: [detect-changes]
if: >-
github.event_name != 'pull_request_review' &&
(github.event_name == 'workflow_dispatch' ||
- needs.detect-changes.outputs.integrations-openclaw == 'true' ||
+ needs.detect-changes.outputs.all-npm == 'true' ||
needs.detect-changes.outputs.ci == 'true')
runs-on: ubuntu-latest
@@ -201,19 +206,71 @@ jobs:
uses: actions/setup-node@v6
with:
node-version: '22'
+ cache: 'npm'
+ cache-dependency-path: package-lock.json
- name: Install dependencies
+ run: npm ci --workspace=hindsight-all-npm
+
+ - name: Run tests
+ run: npm test --workspace=hindsight-all-npm
+
+ - name: Build
+ run: npm run build --workspace=hindsight-all-npm
+
+ build-openclaw-integration:
+ needs: [detect-changes]
+ if: >-
+ github.event_name != 'pull_request_review' &&
+ (github.event_name == 'workflow_dispatch' ||
+ needs.detect-changes.outputs.integrations-openclaw == 'true' ||
+ needs.detect-changes.outputs.clients-ts == 'true' ||
+ needs.detect-changes.outputs.all-npm == 'true' ||
+ needs.detect-changes.outputs.ci == 'true')
+ runs-on: ubuntu-latest
+
+ steps:
+ - uses: actions/checkout@v6
+ with:
+ ref: ${{ github.event.pull_request.head.sha || '' }}
+
+ - name: Set up Node.js
+ uses: actions/setup-node@v6
+ with:
+ node-version: '22'
+ cache: 'npm'
+ cache-dependency-path: package-lock.json
+
+ # openclaw depends on two monorepo workspaces via `file:` deps:
+ # @vectorize-io/hindsight-client and @vectorize-io/hindsight-all. Their
+ # `dist/` directories are gitignored, so we must build them first.
+ # Otherwise vitest/tsc in openclaw fails with
+ # "Failed to resolve entry for package ..." on the value imports.
+ - name: Install root workspace dependencies
+ run: npm ci
+
+ - name: Build hindsight-client (openclaw dep)
+ run: npm run build --workspace=hindsight-clients/typescript
+
+ - name: Build hindsight-all-npm (openclaw dep)
+ run: npm run build --workspace=hindsight-all-npm
+
+ - name: Install openclaw dependencies
working-directory: ./hindsight-integrations/openclaw
run: npm ci
+ # Build must run before tests: one unit test in src/backfill.test.ts
+ # creates a symlink to `$cwd/dist/backfill.js` and calls realpathSync on
+ # it via isDirectExecution(). Without a populated dist/ the realpath call
+ # throws, both paths stay unresolved, and the equality assertion fails.
+ - name: Build
+ working-directory: ./hindsight-integrations/openclaw
+ run: npm run build
+
- name: Run tests
working-directory: ./hindsight-integrations/openclaw
run: npm test
- - name: Build
- working-directory: ./hindsight-integrations/openclaw
- run: npm run build
-
test-claude-code-integration:
needs: [detect-changes]
if: >-
@@ -1525,6 +1582,18 @@ jobs:
print('Models downloaded successfully')
"
+ # openclaw depends on @vectorize-io/hindsight-client and
+ # @vectorize-io/hindsight-all via `file:` — their `dist/` directories are
+ # gitignored and must be built before openclaw's npm ci copies them.
+ - name: Install root workspace dependencies
+ run: npm ci
+
+ - name: Build hindsight-client (openclaw dep)
+ run: npm run build --workspace=hindsight-clients/typescript
+
+ - name: Build hindsight-all-npm (openclaw dep)
+ run: npm run build --workspace=hindsight-all-npm
+
- name: Install openclaw integration dependencies
working-directory: ./hindsight-integrations/openclaw
run: npm ci
@@ -2524,8 +2593,7 @@ jobs:
core.setOutput('run_url', runUrl);
- name: Report status to PR
- if: github.event.pull_request.head.repo.full_name == github.repository
- uses: actions/github-script@v7
+ uses: actions/github-script@v8
with:
script: |
await github.rest.repos.createCommitStatus({
@@ -2568,4 +2636,4 @@ jobs:
issue_number: prNumber,
body,
});
- }
+ }
\ No newline at end of file
diff --git a/hindsight-all-npm/.gitignore b/hindsight-all-npm/.gitignore
new file mode 100644
index 00000000..04f9a0dc
--- /dev/null
+++ b/hindsight-all-npm/.gitignore
@@ -0,0 +1,4 @@
+node_modules
+dist
+*.tgz
+.DS_Store
diff --git a/hindsight-all-npm/README.md b/hindsight-all-npm/README.md
new file mode 100644
index 00000000..fcbacfe7
--- /dev/null
+++ b/hindsight-all-npm/README.md
@@ -0,0 +1,80 @@
+# @vectorize-io/hindsight-all
+
+Node.js equivalent of the Python [`hindsight-all`](https://pypi.org/project/hindsight-all/) package — programmatic lifecycle manager for a local Hindsight daemon. Use this when you want to embed Hindsight in a Node application without hand-rolling subprocess management.
+
+This package deliberately does **not** ship an HTTP client. Once the daemon is running, talk to it with [`@vectorize-io/hindsight-client`](https://www.npmjs.com/package/@vectorize-io/hindsight-client) against `server.getBaseUrl()`. The two packages compose — one owns the daemon process, the other owns the HTTP API surface.
+
+## Requirements
+
+- **Node.js >= 22** — uses global `fetch` and `AbortSignal.timeout`.
+- **`uv` / `uvx`** on `PATH` — used to download and run the underlying `hindsight-embed` daemon on first use. Install via .
+
+## Install
+
+```bash
+npm install @vectorize-io/hindsight-all @vectorize-io/hindsight-client
+```
+
+## Example
+
+```ts
+import { HindsightServer, consoleLogger } from '@vectorize-io/hindsight-all';
+import { HindsightClient } from '@vectorize-io/hindsight-client';
+
+const server = new HindsightServer({
+ profile: 'my-app',
+ port: 9077,
+ env: {
+ HINDSIGHT_API_LLM_PROVIDER: 'anthropic',
+ HINDSIGHT_API_LLM_API_KEY: process.env.ANTHROPIC_API_KEY,
+ HINDSIGHT_API_LLM_MODEL: 'claude-sonnet-4-20250514',
+ HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: '0',
+ },
+ logger: consoleLogger,
+});
+
+await server.start();
+
+const client = new HindsightClient({ baseUrl: server.getBaseUrl() });
+
+await client.retain('user-123', 'User prefers dark mode and concise answers.', {
+ documentId: 'pref-2026-04-01',
+});
+
+const recall = await client.recall('user-123', 'what are the user preferences?');
+console.log(recall.results);
+
+await server.stop();
+```
+
+For a remote Hindsight API, skip `HindsightServer` entirely and just point `HindsightClient` at the remote URL.
+
+## Open config — forward-compatible with new daemon flags
+
+`HindsightServerOptions` is designed so every new environment variable or CLI flag in the underlying Hindsight daemon can be used without waiting for a wrapper release:
+
+- **`env`** accepts an arbitrary `Record`. Every entry is exported into the daemon process and written into the profile config via `--env KEY=VALUE`.
+- **`extraProfileCreateArgs`** / **`extraDaemonStartArgs`** append raw args to the respective commands.
+
+## Development against a local checkout
+
+If you're hacking on the Python `hindsight-embed` package in the same monorepo, point the server at the local path — it'll use `uv run --directory ` instead of `uvx`:
+
+```ts
+new HindsightServer({
+ embedPackagePath: '/path/to/hindsight-embed',
+ // ...
+});
+```
+
+## API surface
+
+- `HindsightServer` — daemon lifecycle (`start`, `stop`, `checkHealth`, `getBaseUrl`, `getProfile`).
+- `Logger` interface plus `silentLogger` (default) and `consoleLogger` helpers.
+- `getEmbedCommand(opts)` — low-level helper that returns the `[cmd, ...args]` tuple used to invoke the underlying Python CLI.
+
+For memory operations (retain, recall, reflect, bank management, stats) use [`@vectorize-io/hindsight-client`](https://www.npmjs.com/package/@vectorize-io/hindsight-client).
+
+## License
+
+MIT
diff --git a/hindsight-all-npm/package.json b/hindsight-all-npm/package.json
new file mode 100644
index 00000000..16729168
--- /dev/null
+++ b/hindsight-all-npm/package.json
@@ -0,0 +1,57 @@
+{
+ "name": "@vectorize-io/hindsight-all",
+ "version": "0.5.0",
+ "description": "Node.js programmatic lifecycle manager for Hindsight — embeds a local hindsight daemon in a Node application. Pair with @vectorize-io/hindsight-client for memory operations.",
+ "main": "dist/index.js",
+ "types": "dist/index.d.ts",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./dist/index.d.ts",
+ "import": "./dist/index.js"
+ }
+ },
+ "keywords": [
+ "hindsight",
+ "hindsight-all",
+ "memory",
+ "ai",
+ "agent",
+ "long-term-memory",
+ "llm",
+ "embedded-server"
+ ],
+ "author": "Vectorize ",
+ "license": "MIT",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/vectorize-io/hindsight.git",
+ "directory": "hindsight-all-npm"
+ },
+ "files": [
+ "dist",
+ "README.md"
+ ],
+ "scripts": {
+ "build": "tsup",
+ "dev": "tsup --watch",
+ "clean": "rm -rf dist",
+ "test": "vitest run src",
+ "test:watch": "vitest src",
+ "prepublishOnly": "npm run clean && npm run build"
+ },
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "tsup": "^8.5.1",
+ "typescript": "^5.7.0",
+ "vitest": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "overrides": {
+ "rollup": "^4.59.0",
+ "picomatch": ">=2.3.2 <3.0.0 || >=4.0.4",
+ "vite": ">=8.0.5"
+ }
+}
diff --git a/hindsight-all-npm/src/command.test.ts b/hindsight-all-npm/src/command.test.ts
new file mode 100644
index 00000000..b47e45bc
--- /dev/null
+++ b/hindsight-all-npm/src/command.test.ts
@@ -0,0 +1,32 @@
+import { describe, it, expect } from 'vitest';
+import { getEmbedCommand } from './command.js';
+
+describe('getEmbedCommand', () => {
+ it('defaults to uvx hindsight-embed@latest', () => {
+ expect(getEmbedCommand()).toEqual(['uvx', 'hindsight-embed@latest']);
+ });
+
+ it('honours an explicit version', () => {
+ expect(getEmbedCommand({ embedVersion: '0.5.0' })).toEqual(['uvx', 'hindsight-embed@0.5.0']);
+ });
+
+ it('treats an empty version as latest', () => {
+ expect(getEmbedCommand({ embedVersion: '' })).toEqual(['uvx', 'hindsight-embed@latest']);
+ });
+
+ it('uses uv run --directory when a local path is given', () => {
+ expect(getEmbedCommand({ embedPackagePath: '/abs/path' })).toEqual([
+ 'uv',
+ 'run',
+ '--directory',
+ '/abs/path',
+ 'hindsight-embed',
+ ]);
+ });
+
+ it('local path takes precedence over version', () => {
+ expect(
+ getEmbedCommand({ embedPackagePath: '/abs/path', embedVersion: '0.5.0' }),
+ ).toEqual(['uv', 'run', '--directory', '/abs/path', 'hindsight-embed']);
+ });
+});
diff --git a/hindsight-all-npm/src/command.ts b/hindsight-all-npm/src/command.ts
new file mode 100644
index 00000000..b959076c
--- /dev/null
+++ b/hindsight-all-npm/src/command.ts
@@ -0,0 +1,25 @@
+/**
+ * Resolve the command that invokes the `hindsight-embed` Python CLI.
+ *
+ * - If `embedPackagePath` is set, runs the package from a local checkout via
+ * `uv run --directory hindsight-embed`. Used for in-repo development.
+ * - Otherwise runs it via `uvx hindsight-embed@` so no global install
+ * is required.
+ *
+ * Returns the argv as `[command, ...baseArgs]` suitable for `spawn()` /
+ * `execFile()` (never shell-interpolated).
+ */
+export interface EmbedCommandOptions {
+ /** Version spec passed to uvx (e.g. "latest", "0.5.0"). Default: "latest". */
+ embedVersion?: string;
+ /** Local checkout path. When set, overrides `embedVersion` and uses `uv run`. */
+ embedPackagePath?: string;
+}
+
+export function getEmbedCommand(opts: EmbedCommandOptions = {}): string[] {
+ if (opts.embedPackagePath) {
+ return ['uv', 'run', '--directory', opts.embedPackagePath, 'hindsight-embed'];
+ }
+ const version = opts.embedVersion && opts.embedVersion.length > 0 ? opts.embedVersion : 'latest';
+ return ['uvx', `hindsight-embed@${version}`];
+}
diff --git a/hindsight-all-npm/src/index.ts b/hindsight-all-npm/src/index.ts
new file mode 100644
index 00000000..5e0ccda3
--- /dev/null
+++ b/hindsight-all-npm/src/index.ts
@@ -0,0 +1,7 @@
+export { HindsightServer } from './server.js';
+export { getEmbedCommand } from './command.js';
+export { silentLogger, consoleLogger } from './logger.js';
+
+export type { Logger } from './logger.js';
+export type { EmbedCommandOptions } from './command.js';
+export type { HindsightServerOptions } from './types.js';
diff --git a/hindsight-all-npm/src/logger.ts b/hindsight-all-npm/src/logger.ts
new file mode 100644
index 00000000..5ce92b46
--- /dev/null
+++ b/hindsight-all-npm/src/logger.ts
@@ -0,0 +1,29 @@
+/**
+ * Pluggable logger interface.
+ *
+ * This package does not own any logging infrastructure — consumers inject
+ * whatever they want (console, pino, openclaw's logger, a no-op). The default
+ * is silent so embedding this package never adds noise to an unrelated app.
+ */
+export interface Logger {
+ debug(msg: string): void;
+ info(msg: string): void;
+ warn(msg: string): void;
+ error(msg: string): void;
+}
+
+/** Logger that drops every call. Used when no logger is passed. */
+export const silentLogger: Logger = {
+ debug: () => {},
+ info: () => {},
+ warn: () => {},
+ error: () => {},
+};
+
+/** Logger that writes to the standard console. Handy for CLIs and tests. */
+export const consoleLogger: Logger = {
+ debug: (msg) => console.debug(msg),
+ info: (msg) => console.log(msg),
+ warn: (msg) => console.warn(msg),
+ error: (msg) => console.error(msg),
+};
diff --git a/hindsight-all-npm/src/server.test.ts b/hindsight-all-npm/src/server.test.ts
new file mode 100644
index 00000000..cac50703
--- /dev/null
+++ b/hindsight-all-npm/src/server.test.ts
@@ -0,0 +1,35 @@
+import { describe, it, expect } from 'vitest';
+import { HindsightServer } from './server.js';
+
+describe('HindsightServer construction', () => {
+ it('defaults base URL to http://127.0.0.1:8888', () => {
+ const server = new HindsightServer();
+ expect(server.getBaseUrl()).toBe('http://127.0.0.1:8888');
+ expect(server.getProfile()).toBe('default');
+ });
+
+ it('honours custom profile, port, and host', () => {
+ const server = new HindsightServer({ profile: 'app', port: 9077, host: '0.0.0.0' });
+ expect(server.getProfile()).toBe('app');
+ expect(server.getBaseUrl()).toBe('http://0.0.0.0:9077');
+ });
+
+ it('accepts open env pass-through without complaining about unknown keys', () => {
+ const server = new HindsightServer({
+ env: {
+ HINDSIGHT_API_LLM_PROVIDER: 'openai',
+ HINDSIGHT_API_LLM_MODEL: 'gpt-4o-mini',
+ // A field that does not exist today — should still be accepted
+ HINDSIGHT_FUTURE_FLAG: 'enabled',
+ },
+ });
+ expect(server).toBeInstanceOf(HindsightServer);
+ });
+
+ it('exposes checkHealth that returns false when no daemon is running', async () => {
+ // Random high port that nothing is listening on.
+ const server = new HindsightServer({ port: 1, readyTimeoutMs: 100 });
+ const healthy = await server.checkHealth();
+ expect(healthy).toBe(false);
+ });
+});
diff --git a/hindsight-all-npm/src/server.ts b/hindsight-all-npm/src/server.ts
new file mode 100644
index 00000000..c258ed39
--- /dev/null
+++ b/hindsight-all-npm/src/server.ts
@@ -0,0 +1,322 @@
+import { spawn } from 'child_process';
+import { getEmbedCommand } from './command.js';
+import { silentLogger } from './logger.js';
+import type { Logger } from './logger.js';
+import type { HindsightServerOptions } from './types.js';
+
+const DEFAULT_PORT = 8888;
+const DEFAULT_HOST = '127.0.0.1';
+const DEFAULT_PROFILE = 'default';
+const DEFAULT_READY_TIMEOUT_MS = 30_000;
+const DEFAULT_READY_POLL_INTERVAL_MS = 1_000;
+
+/**
+ * Manages the lifecycle of a local Hindsight daemon from a Node.js process.
+ *
+ * On {@link start}, this class:
+ * 1. Resolves the `hindsight-embed` command (via `uvx` or a local `uv run`).
+ * 2. Runs `profile create --merge --port [--env K=V ...]`
+ * with every entry in {@link HindsightServerOptions.env} forwarded as
+ * an `--env` flag.
+ * 3. Runs `daemon --profile start` and waits for the start command
+ * to exit.
+ * 4. Polls `http://host:port/health` until it returns `200` or the
+ * `readyTimeoutMs` budget is exhausted.
+ *
+ * On {@link stop}, it runs `daemon --profile stop` and returns once
+ * the command exits (or after a short grace period).
+ *
+ * This is the Node.js equivalent of the Python `hindsight-all` package's
+ * `HindsightServer`: a thin programmatic lifecycle wrapper around the
+ * Hindsight daemon. It does NOT ship an HTTP client — once `start()`
+ * resolves, use `@vectorize-io/hindsight-client` against `getBaseUrl()` for
+ * retain / recall / reflect.
+ *
+ * The class is deliberately transparent about the daemon: new CLI flags or
+ * environment variables never require a code change here — callers can pass
+ * them via `env`, `extraProfileCreateArgs`, or `extraDaemonStartArgs`.
+ */
+export class HindsightServer {
+ private readonly profile: string;
+ private readonly port: number;
+ private readonly host: string;
+ private readonly baseUrl: string;
+ private readonly embedVersion: string | undefined;
+ private readonly embedPackagePath: string | undefined;
+ private readonly userEnv: Record;
+ private readonly extraProfileCreateArgs: string[];
+ private readonly extraDaemonStartArgs: string[];
+ private readonly platformCpuWorkaround: boolean;
+ private readonly readyTimeoutMs: number;
+ private readonly readyPollIntervalMs: number;
+ private readonly logger: Logger;
+
+ constructor(opts: HindsightServerOptions = {}) {
+ this.profile = opts.profile ?? DEFAULT_PROFILE;
+ this.port = opts.port ?? DEFAULT_PORT;
+ this.host = opts.host ?? DEFAULT_HOST;
+ this.baseUrl = `http://${this.host}:${this.port}`;
+ this.embedVersion = opts.embedVersion;
+ this.embedPackagePath = opts.embedPackagePath;
+ this.userEnv = opts.env ?? {};
+ this.extraProfileCreateArgs = opts.extraProfileCreateArgs ?? [];
+ this.extraDaemonStartArgs = opts.extraDaemonStartArgs ?? [];
+ this.platformCpuWorkaround = opts.platformCpuWorkaround ?? (process.platform === 'darwin');
+ this.readyTimeoutMs = opts.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS;
+ this.readyPollIntervalMs = opts.readyPollIntervalMs ?? DEFAULT_READY_POLL_INTERVAL_MS;
+ this.logger = opts.logger ?? silentLogger;
+ }
+
+ /** The base URL the daemon listens on (`http://host:port`). */
+ getBaseUrl(): string {
+ return this.baseUrl;
+ }
+
+ /** The profile name this server operates on. */
+ getProfile(): string {
+ return this.profile;
+ }
+
+ /**
+ * Ensure the daemon is configured and running. Idempotent — the underlying
+ * `profile create --merge` and `daemon start` commands tolerate re-runs.
+ */
+ async start(): Promise {
+ this.logger.info(`[hindsight] starting daemon for profile "${this.profile}"`);
+
+ const env = this.buildEnv();
+ await this.configureProfile(env);
+ await this.startDaemon(env);
+ await this.waitForReady();
+
+ this.logger.info(`[hindsight] daemon ready at ${this.baseUrl}`);
+ }
+
+ /** Stop the daemon. Never throws — logs and resolves even on failure. */
+ async stop(): Promise {
+ this.logger.info(`[hindsight] stopping daemon for profile "${this.profile}"`);
+
+ const [cmd, ...baseArgs] = getEmbedCommand({
+ embedVersion: this.embedVersion,
+ embedPackagePath: this.embedPackagePath,
+ });
+ const args = [...baseArgs, 'daemon', '--profile', this.profile, 'stop'];
+
+ const child = spawn(cmd, args, { stdio: 'pipe' });
+ this.pipeOutput(child, 'daemon.stop');
+
+ await new Promise((resolve) => {
+ const timeout = setTimeout(() => {
+ this.logger.warn(`[hindsight] daemon stop timed out after 5s`);
+ resolve();
+ }, 5_000);
+ child.on('exit', () => {
+ clearTimeout(timeout);
+ this.logger.info(`[hindsight] daemon stopped`);
+ resolve();
+ });
+ child.on('error', (err) => {
+ clearTimeout(timeout);
+ this.logger.warn(`[hindsight] error stopping daemon: ${err.message}`);
+ resolve();
+ });
+ });
+ }
+
+ /** Probe `/health` once with a short timeout. */
+ async checkHealth(): Promise {
+ try {
+ const res = await fetch(`${this.baseUrl}/health`, {
+ signal: AbortSignal.timeout(2_000),
+ });
+ return res.ok;
+ } catch {
+ return false;
+ }
+ }
+
+ // -------------------------------------------------------------------------
+ // Internal
+ // -------------------------------------------------------------------------
+
+ /**
+ * Merge the process env, the caller-supplied `env`, and (on macOS) the
+ * embeddings CPU workaround. Caller-supplied values always win over the
+ * workaround; undefined values are dropped.
+ */
+ private buildEnv(): NodeJS.ProcessEnv {
+ const merged: NodeJS.ProcessEnv = { ...process.env };
+
+ if (this.platformCpuWorkaround && process.platform === 'darwin') {
+ merged['HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU'] = '1';
+ merged['HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU'] = '1';
+ }
+
+ for (const [key, value] of Object.entries(this.userEnv)) {
+ if (value !== undefined) {
+ merged[key] = value;
+ }
+ }
+
+ return merged;
+ }
+
+ /**
+ * Run `profile create --merge --port [--env K=V ...]`.
+ * Every entry in the merged env that was passed via {@link userEnv} (or
+ * auto-applied by the CPU workaround) is forwarded as `--env`.
+ */
+ private async configureProfile(env: NodeJS.ProcessEnv): Promise {
+ this.logger.info(`[hindsight] configuring profile "${this.profile}"`);
+
+ const [cmd, ...baseArgs] = getEmbedCommand({
+ embedVersion: this.embedVersion,
+ embedPackagePath: this.embedPackagePath,
+ });
+ const createArgs = [
+ ...baseArgs,
+ 'profile',
+ 'create',
+ this.profile,
+ '--merge',
+ '--port',
+ String(this.port),
+ ];
+
+ // Forward every env var that the caller intended for the daemon as --env.
+ // We only forward keys the caller explicitly set (userEnv) plus the CPU
+ // workaround values — not the entire process.env, to avoid leaking random
+ // host state into profile config.
+ const envForProfile = this.collectProfileEnv(env);
+ for (const [key, value] of Object.entries(envForProfile)) {
+ createArgs.push('--env', `${key}=${value}`);
+ }
+
+ createArgs.push(...this.extraProfileCreateArgs);
+
+ await this.runCommand(cmd, createArgs, env, 'profile.create');
+ }
+
+ /** Collect only the env vars that should be written into the profile file. */
+ private collectProfileEnv(env: NodeJS.ProcessEnv): Record {
+ const out: Record = {};
+
+ // 1. User-supplied env — always forwarded.
+ for (const [key, value] of Object.entries(this.userEnv)) {
+ if (value !== undefined) {
+ out[key] = value;
+ }
+ }
+
+ // 2. CPU workaround — only if auto-applied and not already overridden.
+ if (this.platformCpuWorkaround && process.platform === 'darwin') {
+ const cpuKeys = [
+ 'HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU',
+ 'HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU',
+ ];
+ for (const key of cpuKeys) {
+ if (!(key in out) && env[key] !== undefined) {
+ out[key] = env[key] as string;
+ }
+ }
+ }
+
+ return out;
+ }
+
+ private async startDaemon(env: NodeJS.ProcessEnv): Promise {
+ const [cmd, ...baseArgs] = getEmbedCommand({
+ embedVersion: this.embedVersion,
+ embedPackagePath: this.embedPackagePath,
+ });
+ const args = [
+ ...baseArgs,
+ 'daemon',
+ '--profile',
+ this.profile,
+ 'start',
+ ...this.extraDaemonStartArgs,
+ ];
+
+ await this.runCommand(cmd, args, env, 'daemon.start');
+ }
+
+ /**
+ * Spawn `cmd` with `args`, pipe its output through the logger, and resolve
+ * once it exits with code 0. Rejects on non-zero exit or spawn error.
+ */
+ private async runCommand(
+ cmd: string,
+ args: string[],
+ env: NodeJS.ProcessEnv,
+ label: string,
+ ): Promise {
+ const child = spawn(cmd, args, { stdio: 'pipe', env });
+ let output = '';
+ child.stdout?.on('data', (data: Buffer) => {
+ const text = data.toString();
+ output += text;
+ for (const line of text.trimEnd().split('\n')) {
+ if (line) this.logger.info(`[hindsight:${label}] ${line}`);
+ }
+ });
+ child.stderr?.on('data', (data: Buffer) => {
+ const text = data.toString();
+ output += text;
+ for (const line of text.trimEnd().split('\n')) {
+ if (line) this.logger.warn(`[hindsight:${label}] ${line}`);
+ }
+ });
+
+ await new Promise((resolve, reject) => {
+ child.on('exit', (code) => {
+ if (code === 0) {
+ resolve();
+ } else {
+ reject(new Error(`${label} failed with code ${code}: ${output.trim()}`));
+ }
+ });
+ child.on('error', (err) => {
+ reject(new Error(`${label} failed to spawn: ${err.message}`, { cause: err }));
+ });
+ });
+ }
+
+ /** Stream a spawned child's stdout/stderr through the logger without blocking. */
+ private pipeOutput(child: ReturnType, label: string): void {
+ child.stdout?.on('data', (data: Buffer) => {
+ for (const line of data.toString().trimEnd().split('\n')) {
+ if (line) this.logger.info(`[hindsight:${label}] ${line}`);
+ }
+ });
+ child.stderr?.on('data', (data: Buffer) => {
+ for (const line of data.toString().trimEnd().split('\n')) {
+ if (line) this.logger.warn(`[hindsight:${label}] ${line}`);
+ }
+ });
+ }
+
+ /** Poll `/health` until it succeeds or `readyTimeoutMs` elapses. */
+ private async waitForReady(): Promise {
+ const deadline = Date.now() + this.readyTimeoutMs;
+ let attempt = 0;
+ while (Date.now() < deadline) {
+ attempt++;
+ try {
+ const res = await fetch(`${this.baseUrl}/health`, {
+ signal: AbortSignal.timeout(this.readyPollIntervalMs),
+ });
+ if (res.ok) {
+ this.logger.debug(`[hindsight] health check passed (attempt ${attempt})`);
+ return;
+ }
+ } catch {
+ // expected while the daemon is still booting
+ }
+ await new Promise((resolve) => setTimeout(resolve, this.readyPollIntervalMs));
+ }
+ throw new Error(
+ `Hindsight daemon did not become ready within ${this.readyTimeoutMs}ms at ${this.baseUrl}`,
+ );
+ }
+}
diff --git a/hindsight-all-npm/src/types.ts b/hindsight-all-npm/src/types.ts
new file mode 100644
index 00000000..ec7f0527
--- /dev/null
+++ b/hindsight-all-npm/src/types.ts
@@ -0,0 +1,54 @@
+import type { Logger } from './logger.js';
+
+/**
+ * Options for {@link HindsightServer}.
+ *
+ * The server is intentionally thin and pass-through: anything configurable
+ * on the daemon side (env vars or CLI flags) can be set here without needing
+ * a new dedicated option. Use {@link env} for `HINDSIGHT_*` / `OPENAI_API_KEY` /
+ * custom provider settings, and the two `extra*` arrays to append raw CLI
+ * args to `profile create` or `daemon start`.
+ *
+ * For talking to the daemon after `start()`, use `@vectorize-io/hindsight-client`
+ * against `server.getBaseUrl()`. This package does not ship its own HTTP
+ * client.
+ */
+export interface HindsightServerOptions {
+ /** Profile name used for `--profile ` on every sub-command. Default: `"default"`. */
+ profile?: string;
+ /** TCP port the daemon listens on. Default: `8888`. */
+ port?: number;
+ /** Hostname the daemon binds to (for health checks). Default: `127.0.0.1`. */
+ host?: string;
+ /** Version of the underlying `hindsight-embed` PyPI package to run via `uvx`. Default: `"latest"`. */
+ embedVersion?: string;
+ /** Local path to a `hindsight-embed` checkout — takes precedence over `embedVersion`. */
+ embedPackagePath?: string;
+ /**
+ * Environment variables passed to the daemon process AND written into the
+ * profile via repeated `--env KEY=VALUE` flags. This is the preferred way
+ * to surface any `HINDSIGHT_API_*` / `HINDSIGHT_EMBED_*` setting — adding a
+ * new daemon env var never requires a wrapper update.
+ *
+ * Values of `undefined` are dropped (so you can spread conditionally).
+ */
+ env?: Record;
+ /** Extra args appended verbatim to `hindsight-embed profile create --merge ...`. */
+ extraProfileCreateArgs?: string[];
+ /** Extra args appended verbatim to `hindsight-embed daemon --profile start ...`. */
+ extraDaemonStartArgs?: string[];
+ /**
+ * On macOS, automatically set
+ * `HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=1` and
+ * `HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=1` to avoid Metal/MPS crashes in
+ * daemon mode. Default: `true` on `darwin`, ignored elsewhere. Any value set
+ * explicitly in {@link env} wins over the auto-applied value.
+ */
+ platformCpuWorkaround?: boolean;
+ /** Max time (ms) to wait for `/health` to return 200. Default: `30_000`. */
+ readyTimeoutMs?: number;
+ /** Polling interval (ms) while waiting for `/health`. Default: `1_000`. */
+ readyPollIntervalMs?: number;
+ /** Optional pluggable logger. Default: silent. */
+ logger?: Logger;
+}
diff --git a/hindsight-all-npm/tsconfig.json b/hindsight-all-npm/tsconfig.json
new file mode 100644
index 00000000..d1f4cf02
--- /dev/null
+++ b/hindsight-all-npm/tsconfig.json
@@ -0,0 +1,18 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "ES2022",
+ "lib": ["ES2022"],
+ "moduleResolution": "node",
+ "declaration": true,
+ "outDir": "./dist",
+ "rootDir": "./src",
+ "strict": true,
+ "esModuleInterop": true,
+ "skipLibCheck": true,
+ "forceConsistentCasingInFileNames": true,
+ "resolveJsonModule": true
+ },
+ "include": ["src/**/*"],
+ "exclude": ["node_modules", "dist", "src/**/*.test.ts"]
+}
diff --git a/hindsight-all-npm/tsup.config.ts b/hindsight-all-npm/tsup.config.ts
new file mode 100644
index 00000000..a3cfbf07
--- /dev/null
+++ b/hindsight-all-npm/tsup.config.ts
@@ -0,0 +1,11 @@
+import { defineConfig } from 'tsup';
+
+export default defineConfig({
+ entry: ['src/index.ts'],
+ format: ['esm'],
+ dts: true,
+ outDir: 'dist',
+ clean: true,
+ sourcemap: true,
+ bundle: true,
+});
diff --git a/hindsight-all-npm/vitest.config.ts b/hindsight-all-npm/vitest.config.ts
new file mode 100644
index 00000000..96eb6ab4
--- /dev/null
+++ b/hindsight-all-npm/vitest.config.ts
@@ -0,0 +1,8 @@
+import { defineConfig } from 'vitest/config';
+
+export default defineConfig({
+ test: {
+ include: ['src/**/*.test.ts'],
+ environment: 'node',
+ },
+});
diff --git a/hindsight-docs/docs/sdks/embed.md b/hindsight-docs/docs/sdks/embed.md
index 7b1ba402..53512559 100644
--- a/hindsight-docs/docs/sdks/embed.md
+++ b/hindsight-docs/docs/sdks/embed.md
@@ -2,7 +2,7 @@
sidebar_position: 5
---
-# Embedded SDK (hindsight-embed)
+# Daemon CLI (hindsight-embed)
Zero-configuration local memory system with automatic daemon management. Perfect for development, prototyping, and single-user applications.
diff --git a/hindsight-docs/docs/sdks/hindsight-all-npm.md b/hindsight-docs/docs/sdks/hindsight-all-npm.md
new file mode 100644
index 00000000..72e61d28
--- /dev/null
+++ b/hindsight-docs/docs/sdks/hindsight-all-npm.md
@@ -0,0 +1,90 @@
+---
+sidebar_position: 6
+---
+
+# Programmatic API (Node.js)
+
+The `@vectorize-io/hindsight-all` npm package is the Node.js equivalent of the Python [`hindsight-all`](./hindsight-all.md) package. It lets your Node code spawn and supervise a local Hindsight daemon without deploying any server infrastructure — pair it with [`@vectorize-io/hindsight-client`](./nodejs.md) for memory operations.
+
+The daemon runs as a **separate OS process** on `127.0.0.1` (not in your Node process). Your code talks to it over HTTP via `HindsightClient`.
+
+This package **does not ship an HTTP client** — it only owns the server process. Once the daemon is running, talk to it with [`@vectorize-io/hindsight-client`](./nodejs.md) against `server.getBaseUrl()`. The two packages compose: one owns the process, the other owns the API surface.
+
+## How it works
+
+1. `server.start()` resolves the underlying `hindsight-embed` command (via `uvx` from PyPI, or `uv run --directory ` for a local checkout).
+2. Runs `profile create --merge --port [--env KEY=VALUE ...]` with every entry from `options.env` forwarded as `--env`.
+3. Runs `daemon --profile start`.
+4. Polls `http://host:port/health` until it returns `200` or the `readyTimeoutMs` budget is exhausted.
+5. `server.stop()` runs `daemon --profile stop`.
+
+The server is intentionally transparent: new daemon env vars or CLI flags never require a wrapper release — pass them through `env`, `extraProfileCreateArgs`, or `extraDaemonStartArgs`.
+
+## Requirements
+
+- **Node.js ≥ 22** — uses global `fetch` and `AbortSignal.timeout`.
+- **`uv` / `uvx`** on `PATH` — used to download and run the Hindsight daemon. Install via [docs.astral.sh/uv](https://docs.astral.sh/uv/).
+
+## Install
+
+```bash
+npm install @vectorize-io/hindsight-all @vectorize-io/hindsight-client
+```
+
+## Example
+
+```ts
+import { HindsightServer, consoleLogger } from '@vectorize-io/hindsight-all';
+import { HindsightClient } from '@vectorize-io/hindsight-client';
+
+const server = new HindsightServer({
+ profile: 'my-app',
+ port: 9077,
+ env: {
+ HINDSIGHT_API_LLM_PROVIDER: 'anthropic',
+ HINDSIGHT_API_LLM_API_KEY: process.env.ANTHROPIC_API_KEY,
+ HINDSIGHT_API_LLM_MODEL: 'claude-sonnet-4-20250514',
+ HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: '0',
+ },
+ logger: consoleLogger,
+});
+
+await server.start();
+
+const client = new HindsightClient({ baseUrl: server.getBaseUrl() });
+await client.retain('user-123', 'User prefers dark mode.');
+const recall = await client.recall('user-123', 'what are the user preferences?');
+
+await server.stop();
+```
+
+For a remote Hindsight API, skip the server entirely and point `HindsightClient` directly at the remote URL.
+
+## `HindsightServerOptions`
+
+| Option | Type | Default | Description |
+|---|---|---|---|
+| `profile` | `string` | `"default"` | Profile name passed to `--profile` on every sub-command. |
+| `port` | `number` | `8888` | TCP port the daemon listens on. |
+| `host` | `string` | `"127.0.0.1"` | Hostname the daemon binds to (used for health checks). |
+| `embedVersion` | `string` | `"latest"` | Version of the underlying `hindsight-embed` package to run via `uvx`. |
+| `embedPackagePath` | `string` | — | Local checkout path — takes precedence over `embedVersion`. Uses `uv run --directory` instead of `uvx`. |
+| `env` | `Record` | `{}` | Environment variables passed to the daemon process **and** written into the profile config via `--env KEY=VALUE`. The preferred way to surface any `HINDSIGHT_API_*` / `HINDSIGHT_EMBED_*` setting. |
+| `extraProfileCreateArgs` | `string[]` | `[]` | Extra args appended verbatim to `profile create`. |
+| `extraDaemonStartArgs` | `string[]` | `[]` | Extra args appended verbatim to `daemon start`. |
+| `platformCpuWorkaround` | `boolean` | `true` on macOS | Auto-set `HINDSIGHT_API_EMBEDDINGS_LOCAL_FORCE_CPU=1` and `HINDSIGHT_API_RERANKER_LOCAL_FORCE_CPU=1` to avoid Metal/MPS crashes. Caller-supplied `env` values win over the auto-applied ones. |
+| `readyTimeoutMs` | `number` | `30000` | Max time to wait for `/health` to return 200. |
+| `readyPollIntervalMs` | `number` | `1000` | Polling interval while waiting for `/health`. |
+| `logger` | `Logger` | silent | Pluggable logger (`debug`/`info`/`warn`/`error`). `consoleLogger` and `silentLogger` helpers are exported. |
+
+## Server methods
+
+| Method | Returns | Description |
+|---|---|---|
+| `start()` | `Promise` | Configure profile, spawn the daemon, wait for `/health`. Idempotent — safe to re-run. |
+| `stop()` | `Promise` | Stop the daemon. Never throws; logs and resolves even on failure. |
+| `checkHealth()` | `Promise` | One-shot `/health` probe with a 2 s timeout. |
+| `getBaseUrl()` | `string` | `http://host:port` — pass this straight to `HindsightClient`. |
+| `getProfile()` | `string` | The profile name this server operates on. |
+
+For memory operations (retain, recall, reflect, bank management) use [`@vectorize-io/hindsight-client`](./nodejs.md).
diff --git a/hindsight-docs/docs/sdks/hindsight-all.md b/hindsight-docs/docs/sdks/hindsight-all.md
new file mode 100644
index 00000000..ee9dde50
--- /dev/null
+++ b/hindsight-docs/docs/sdks/hindsight-all.md
@@ -0,0 +1,146 @@
+---
+sidebar_position: 2
+---
+
+# Programmatic API (Python)
+
+The `hindsight-all` Python package lets your code spawn and manage a local Hindsight daemon without deploying any server infrastructure. It bundles the Hindsight API server, embedded PostgreSQL, and the Python client into one install — `pip install hindsight-all` and you can start a fully-functional Hindsight instance from a few lines of Python.
+
+The daemon runs as a **separate OS process** on `127.0.0.1` (not in your Python process memory). Your code talks to it over HTTP via the bundled `HindsightClient`.
+
+If you already have a Hindsight server running and just need a client, use [Python Client (hindsight-client)](./python.md) instead.
+
+## How it works
+
+`hindsight-all` exposes two primary APIs:
+
+- **`HindsightServer`** — explicit lifecycle. Use it as a context manager when you want deterministic startup/shutdown (e.g. in tests).
+- **`HindsightEmbedded`** — auto-managed. Starts a daemon on first use, reuses it across calls, shuts it down after an idle timeout. Easiest for application code that doesn't want to think about lifecycle.
+
+Both end up talking to the same underlying daemon via the same `HindsightClient` HTTP interface — the difference is only how the server process is managed.
+
+## Installation
+
+```bash
+pip install hindsight-all
+```
+
+The `hindsight-all` wheel bundles `hindsight-api-slim`, `hindsight-client`, and `hindsight-embed` as dependencies, so one `pip install` gets you everything.
+
+## `HindsightServer` — explicit lifecycle
+
+Use `HindsightServer` as a context manager when you want the server to start immediately, run for the duration of a block, and shut down cleanly afterwards. Ideal for tests and short-lived scripts.
+
+```python
+import os
+from hindsight import HindsightServer, HindsightClient
+
+with HindsightServer(
+ llm_provider="openai",
+ llm_model="gpt-4o-mini",
+ llm_api_key=os.environ["OPENAI_API_KEY"],
+) as server:
+ client = HindsightClient(base_url=server.url)
+
+ client.retain(bank_id="my-bank", content="Alice works at Google")
+ results = client.recall(bank_id="my-bank", query="What does Alice do?")
+ for r in results:
+ print(r.text)
+
+ answer = client.reflect(bank_id="my-bank", query="Tell me about Alice")
+ print(answer.text)
+# Server is stopped here
+```
+
+## `HindsightEmbedded` — auto-managed
+
+`HindsightEmbedded` is the simplest way to use Hindsight in Python. It automatically manages a background daemon for you — starts on first use, stays alive across calls, shuts down after an idle timeout.
+
+```python
+from hindsight import HindsightEmbedded
+import os
+
+# Server starts automatically on first call
+client = HindsightEmbedded(
+ profile="myapp", # Profile for data isolation
+ llm_provider="openai",
+ llm_model="gpt-4o-mini",
+ llm_api_key=os.environ["OPENAI_API_KEY"],
+)
+
+# Use immediately - no manual server management needed
+client.retain(bank_id="my-bank", content="Alice works at Google")
+results = client.recall(bank_id="my-bank", query="What does Alice do?")
+
+# Server continues running (auto-stops after idle timeout)
+# Or explicitly stop it:
+client.close(stop_daemon=True)
+```
+
+### What's a Profile?
+
+A profile is an isolated Hindsight environment. Each profile gets its own embedded PostgreSQL database (stored in `~/.pg0/instances/hindsight-embed-{profile}/`) and its own API server. Use different profiles to separate environments (dev/prod), applications, or users.
+
+### When to use which
+
+| Use case | Pick |
+|---|---|
+| Tests, short-lived scripts, deterministic startup/shutdown | `HindsightServer` (context manager) |
+| Long-running application, auto-start on first use, don't want to manage lifecycle | `HindsightEmbedded` |
+| Existing Hindsight server running elsewhere | [`hindsight-client`](./python.md) directly |
+
+## API namespaces
+
+Both `HindsightEmbedded` and `HindsightClient` expose organized API namespaces for bank management, mental models, directives, and memories:
+
+```python
+from hindsight import HindsightEmbedded
+import os
+
+embedded = HindsightEmbedded(
+ profile="myapp",
+ llm_provider="openai",
+ llm_api_key=os.environ["OPENAI_API_KEY"],
+)
+
+# Core operations
+embedded.retain(bank_id="test", content="Hello")
+results = embedded.recall(bank_id="test", query="Hello")
+
+# Bank management
+embedded.banks.create(bank_id="test", name="Test Bank", mission="Help users")
+embedded.banks.set_mission(bank_id="test", mission="Updated mission")
+embedded.banks.delete(bank_id="test")
+
+# Mental models
+embedded.mental_models.create(
+ bank_id="test",
+ name="User Preferences",
+ content="User prefers dark mode"
+)
+models = embedded.mental_models.list(bank_id="test")
+
+# Directives
+embedded.directives.create(
+ bank_id="test",
+ name="Response Style",
+ content="Be concise and friendly"
+)
+directives = embedded.directives.list(bank_id="test")
+
+# List memories
+memories = embedded.memories.list(bank_id="test", type="world", limit=50)
+```
+
+API namespaces ensure the daemon is running before each call, so daemon crashes are handled gracefully:
+
+```python
+# ✅ GOOD - Uses API namespace (daemon restarts handled)
+embedded.banks.create(bank_id="test", name="Test")
+
+# ❌ BAD - Direct client access (daemon crashes NOT handled)
+client = embedded.client
+client.create_bank(bank_id="test", name="Test") # Fails if daemon crashed
+```
+
+For the full reference of retain/recall/reflect methods and their options (which work the same regardless of how you obtain the client) see the [Python Client page](./python.md).
diff --git a/hindsight-docs/docs/sdks/python.md b/hindsight-docs/docs/sdks/python.md
index 1ddfb774..86b4419b 100644
--- a/hindsight-docs/docs/sdks/python.md
+++ b/hindsight-docs/docs/sdks/python.md
@@ -4,66 +4,18 @@ sidebar_position: 1
# Python Client
-Official Python client for the Hindsight API.
+Official HTTP client for the Hindsight API. Use this when you have a Hindsight server already running — locally, in Docker, or as a managed service — and you want a typed Python client to talk to it.
-import Tabs from '@theme/Tabs';
-import TabItem from '@theme/TabItem';
+If you want to **embed and run a Hindsight server in your Python process** (no external server required), see [Embedded Python (hindsight-all)](./hindsight-all.md) instead.
## Installation
-
-
-
-The `hindsight-all` package includes embedded PostgreSQL, HTTP API server, and client:
-
-```bash
-pip install hindsight-all
-```
-
-
-
-
-If you already have a Hindsight server running:
-
```bash
pip install hindsight-client
```
-
-
-
## Quick Start
-
-
-
-```python
-import os
-from hindsight import HindsightServer, HindsightClient
-
-with HindsightServer(
- llm_provider="openai",
- llm_model="gpt-4o-mini",
- llm_api_key=os.environ["OPENAI_API_KEY"]
-) as server:
- client = HindsightClient(base_url=server.url)
-
- # Retain a memory
- client.retain(bank_id="my-bank", content="Alice works at Google")
-
- # Recall memories
- results = client.recall(bank_id="my-bank", query="What does Alice do?")
- for r in results:
- print(r.text)
-
- # Reflect - generate response with disposition
- answer = client.reflect(bank_id="my-bank", query="Tell me about Alice")
- print(answer.text)
-```
-
-
-
-
```python
from hindsight_client import Hindsight
@@ -74,130 +26,36 @@ client.retain(bank_id="my-bank", content="Alice works at Google")
# Recall memories
results = client.recall(bank_id="my-bank", query="What does Alice do?")
-for r in results:
+for r in results.results:
print(r.text)
-# Reflect - generate response with disposition
+# Reflect - generate a contextual answer
answer = client.reflect(bank_id="my-bank", query="Tell me about Alice")
print(answer.text)
```
-
-
-
-## Embedded Client (Easiest Option)
-
-`HindsightEmbedded` provides the simplest way to use Hindsight in Python. It automatically manages a background server for you - no manual setup required:
-
-```python
-from hindsight import HindsightEmbedded
-import os
-
-# Server starts automatically on first use
-client = HindsightEmbedded(
- profile="myapp", # Profile for data isolation
- llm_provider="openai",
- llm_model="gpt-4o-mini",
- llm_api_key=os.environ["OPENAI_API_KEY"],
-)
-
-# Use immediately - no manual server management needed
-client.retain(bank_id="my-bank", content="Alice works at Google")
-results = client.recall(bank_id="my-bank", query="What does Alice do?")
-
-# Server continues running (auto-stops after idle timeout)
-# Or explicitly stop it:
-client.close(stop_daemon=True)
-```
-
-**What's a Profile?**
-
-A profile is an isolated Hindsight environment. Each profile gets its own PostgreSQL database (stored in `~/.pg0/instances/hindsight-embed-{profile}/`) and its own API server. Use different profiles to separate environments (dev/prod), applications, or users.
-
-**When to Use HindsightEmbedded**
-
-Use `HindsightEmbedded` when you want the server to start automatically and manage itself. Use `HindsightServer` when you need explicit control over server lifecycle (e.g., testing where you want immediate startup/shutdown).
-
-**Advanced Operations**
-
-`HindsightEmbedded` provides organized API namespaces for advanced operations. Each method call automatically ensures the daemon is running:
-
-```python
-from hindsight import HindsightEmbedded
-import os
-
-embedded = HindsightEmbedded(
- profile="myapp",
- llm_provider="openai",
- llm_api_key=os.environ["OPENAI_API_KEY"],
-)
-
-# Core operations (automatically proxied)
-embedded.retain(bank_id="test", content="Hello")
-results = embedded.recall(bank_id="test", query="Hello")
-
-# Bank management
-embedded.banks.create(bank_id="test", name="Test Bank", mission="Help users")
-embedded.banks.set_mission(bank_id="test", mission="Updated mission")
-embedded.banks.delete(bank_id="test")
-
-# Mental models
-embedded.mental_models.create(
- bank_id="test",
- name="User Preferences",
- content="User prefers dark mode"
-)
-models = embedded.mental_models.list(bank_id="test")
-embedded.mental_models.update(bank_id="test", mental_model_id="...", content="New content")
-
-# Directives
-embedded.directives.create(
- bank_id="test",
- name="Response Style",
- content="Be concise and friendly"
-)
-directives = embedded.directives.list(bank_id="test")
-
-# List memories
-memories = embedded.memories.list(bank_id="test", type="world", limit=50)
-```
-
-**Why Use API Namespaces?**
-
-API namespaces (`banks`, `mental_models`, `directives`, `memories`) ensure the daemon is running before each call. This handles daemon crashes gracefully:
-
-```python
-# ✅ GOOD - Uses API namespace (daemon restarts handled)
-embedded.banks.create(bank_id="test", name="Test")
-
-# ❌ BAD - Direct client access (daemon crashes NOT handled)
-client = embedded.client
-client.create_bank(bank_id="test", name="Test") # Fails if daemon crashed
-```
-
## Client Initialization
```python
-from hindsight import HindsightClient
+from hindsight_client import Hindsight
-client = HindsightClient(
+client = Hindsight(
base_url="http://localhost:8888", # Hindsight API URL
timeout=30.0, # Request timeout in seconds
+ # api_key="your-api-key", # Optional bearer token
)
# Core operations
client.retain(bank_id="test", content="Hello world")
results = client.recall(bank_id="test", query="Hello")
-# Organized API access (same as HindsightEmbedded)
+# Organized API namespaces
client.banks.create(bank_id="test", name="Test Bank")
models = client.mental_models.list(bank_id="test")
directives = client.directives.list(bank_id="test")
memories = client.memories.list(bank_id="test")
```
-Both `HindsightClient` and `HindsightEmbedded` provide the same organized API namespaces (`banks`, `mental_models`, `directives`, `memories`) for consistent developer experience.
-
## Core Operations
### Retain (Store Memory)
diff --git a/hindsight-docs/docusaurus.config.ts b/hindsight-docs/docusaurus.config.ts
index 03233e95..530e384a 100644
--- a/hindsight-docs/docusaurus.config.ts
+++ b/hindsight-docs/docusaurus.config.ts
@@ -83,18 +83,25 @@ const config: Config = {
docs: {
sidebarPath: './sidebars.ts',
routeBasePath: '/',
- // Only show "next" version in development or when INCLUDE_CURRENT_VERSION=true
- // In production, only show released versions from versions.json
+ // Whether to include the "current" (Next / unreleased) docs version.
+ //
+ // Controlled by the single explicit env var INCLUDE_CURRENT_VERSION.
+ // `start-docs.sh` sets it to "true" so local dev always sees Next;
+ // production builds leave it unset so only released versions ship.
+ //
+ // We deliberately do NOT sniff NODE_ENV here — it's unreliable
+ // across Docusaurus hot-reload paths and used to cause the Next
+ // version to disappear intermittently when editing files.
onlyIncludeVersions: (() => {
- const isDev = process.env.NODE_ENV === 'development' || process.env.INCLUDE_CURRENT_VERSION === 'true';
+ const includeCurrent = process.env.INCLUDE_CURRENT_VERSION === 'true';
+ let released: string[] = [];
try {
- const versions = require('./versions.json') as string[];
- // In dev mode, explicitly include 'current' (Next) + all released versions
- // In production, only show released versions
- return isDev ? ['current', ...versions] : versions;
+ released = require('./versions.json') as string[];
} catch {
- return undefined; // No versions yet, show current
+ // No versions.json yet — nothing has been released.
+ return undefined;
}
+ return includeCurrent ? ['current', ...released] : released;
})(),
// Disable version badges on all versions
versions: (() => {
diff --git a/hindsight-docs/sidebars.ts b/hindsight-docs/sidebars.ts
index 483fa59b..5078e648 100644
--- a/hindsight-docs/sidebars.ts
+++ b/hindsight-docs/sidebars.ts
@@ -165,12 +165,6 @@ const sidebars: SidebarsConfig = {
label: 'CLI',
customProps: { icon: 'lu-terminal' },
},
- {
- type: 'doc',
- id: 'sdks/embed',
- label: 'Embedded Python',
- customProps: { icon: '/img/icons/package.svg' },
- },
],
},
{
@@ -355,6 +349,49 @@ const sidebars: SidebarsConfig = {
},
],
},
+ {
+ type: 'category',
+ label: 'Installation',
+ collapsible: false,
+ items: [
+ {
+ type: 'link',
+ href: '/developer/installation#docker',
+ label: 'Docker',
+ customProps: { icon: 'si-docker', iconAfter: 'lu-arrow-up-right' },
+ },
+ {
+ type: 'link',
+ href: '/developer/installation#helm--kubernetes',
+ label: 'Kubernetes',
+ customProps: { icon: 'si-kubernetes', iconAfter: 'lu-arrow-up-right' },
+ },
+ {
+ type: 'link',
+ href: '/developer/installation#bare-metal-pip',
+ label: 'Bare Metal',
+ customProps: { icon: 'lu-hard-drive', iconAfter: 'lu-arrow-up-right' },
+ },
+ {
+ type: 'doc',
+ id: 'sdks/hindsight-all',
+ label: 'Programmatic API (Python)',
+ customProps: { icon: 'si-python' },
+ },
+ {
+ type: 'doc',
+ id: 'sdks/hindsight-all-npm',
+ label: 'Programmatic API (Node.js)',
+ customProps: { icon: 'si-nodedotjs' },
+ },
+ {
+ type: 'doc',
+ id: 'sdks/embed',
+ label: 'Daemon CLI',
+ customProps: { icon: 'lu-terminal' },
+ },
+ ],
+ },
{
type: 'category',
label: 'Resources',
diff --git a/hindsight-docs/src/theme/DocSidebarItem/Link/index.tsx b/hindsight-docs/src/theme/DocSidebarItem/Link/index.tsx
index 10a537e6..d7080428 100644
--- a/hindsight-docs/src/theme/DocSidebarItem/Link/index.tsx
+++ b/hindsight-docs/src/theme/DocSidebarItem/Link/index.tsx
@@ -9,12 +9,12 @@ import {
LuZap, LuDatabase, LuGitCompare, LuRocket, LuMemoryStick,
LuWebhook, LuFileText, LuServer, LuSettings, LuTerminal,
LuActivity, LuPlug, LuShield, LuPackage, LuBook,
- LuNetwork, LuCode, LuLayers, LuCpu,
+ LuNetwork, LuCode, LuLayers, LuCpu, LuHardDrive,
LuArrowUpRight, LuBookOpen, LuRss, LuCloud, LuMessageCircle,
LuChartBar, LuChartColumn, LuStar, LuCircleHelp,
LuLayoutTemplate, LuFileJson,
} from 'react-icons/lu';
-import {SiGo, SiPython, SiGithub, SiSlack} from 'react-icons/si';
+import {SiGo, SiPython, SiGithub, SiSlack, SiDocker, SiKubernetes, SiNodedotjs} from 'react-icons/si';
const ICON_MAP: Record = {
'lu-brain': LuBrain,
@@ -41,10 +41,14 @@ const ICON_MAP: Record = {
'lu-code': LuCode,
'lu-layers': LuLayers,
'lu-cpu': LuCpu,
+ 'lu-hard-drive': LuHardDrive,
'si-go': SiGo,
'si-python': SiPython,
'si-github': SiGithub,
'si-slack': SiSlack,
+ 'si-docker': SiDocker,
+ 'si-kubernetes': SiKubernetes,
+ 'si-nodedotjs': SiNodedotjs,
'lu-chart-bar': LuChartBar,
'lu-chart-column': LuChartColumn,
'lu-arrow-up-right': LuArrowUpRight,
diff --git a/hindsight-integrations/claude-code/scripts/lib/daemon.py b/hindsight-integrations/claude-code/scripts/lib/daemon.py
index 9d8a2585..7b8e4c16 100644
--- a/hindsight-integrations/claude-code/scripts/lib/daemon.py
+++ b/hindsight-integrations/claude-code/scripts/lib/daemon.py
@@ -1,6 +1,6 @@
"""Hindsight-embed daemon lifecycle management.
-Port of: HindsightEmbedManager in embed-manager.js, adapted for Python
+Port of: HindsightServer in @vectorize-io/hindsight-all, adapted for Python
subprocess calls from ephemeral hook processes.
Manages three connection modes (same as Openclaw):
@@ -30,7 +30,7 @@ PROFILE_NAME = "claude-code"
def _get_embed_command(config: dict) -> list:
"""Get the command to run hindsight-embed.
- Port of: getEmbedCommand() in embed-manager.js
+ Port of: getEmbedCommand() in @vectorize-io/hindsight-all
"""
embed_path = config.get("embedPackagePath")
if embed_path:
@@ -139,7 +139,7 @@ def get_api_url(config: dict, debug_fn=None, allow_daemon_start: bool = False) -
def _ensure_daemon_running(config: dict, port: int, debug_fn=None):
"""Start the hindsight-embed daemon if not already running.
- Port of: HindsightEmbedManager.start() in embed-manager.js
+ Port of: HindsightServer.start() in @vectorize-io/hindsight-all
"""
# Fast-fail if hindsight-embed toolchain is not available
if not _is_embed_available(config):
diff --git a/hindsight-integrations/openclaw/package-lock.json b/hindsight-integrations/openclaw/package-lock.json
index 6e2cdc29..d7eed59f 100644
--- a/hindsight-integrations/openclaw/package-lock.json
+++ b/hindsight-integrations/openclaw/package-lock.json
@@ -9,7 +9,11 @@
"version": "0.5.1",
"license": "MIT",
"dependencies": {
- "node-fetch": "^3.3.2"
+ "@vectorize-io/hindsight-all": "file:../../hindsight-all-npm",
+ "@vectorize-io/hindsight-client": "file:../../hindsight-clients/typescript"
+ },
+ "bin": {
+ "hindsight-openclaw-backfill": "dist/backfill.js"
},
"devDependencies": {
"@types/node": "^20.0.0",
@@ -21,6 +25,34 @@
"node": ">=22"
}
},
+ "../../hindsight-all-npm": {
+ "name": "@vectorize-io/hindsight-all",
+ "version": "0.5.0",
+ "license": "MIT",
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "tsup": "^8.5.1",
+ "typescript": "^5.7.0",
+ "vitest": "^4.1.2"
+ },
+ "engines": {
+ "node": ">=22"
+ }
+ },
+ "../../hindsight-clients/typescript": {
+ "name": "@vectorize-io/hindsight-client",
+ "version": "0.5.0",
+ "license": "MIT",
+ "devDependencies": {
+ "@hey-api/openapi-ts": "0.88.0",
+ "@types/jest": "^29.0.0",
+ "@types/node": "^20.0.0",
+ "jest": "^29.0.0",
+ "ts-jest": "^29.0.0",
+ "tsup": "^8.5.1",
+ "typescript": "^5.0.0"
+ }
+ },
"node_modules/@emnapi/core": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz",
@@ -883,6 +915,14 @@
"undici-types": "~6.21.0"
}
},
+ "node_modules/@vectorize-io/hindsight-all": {
+ "resolved": "../../hindsight-all-npm",
+ "link": true
+ },
+ "node_modules/@vectorize-io/hindsight-client": {
+ "resolved": "../../hindsight-clients/typescript",
+ "link": true
+ },
"node_modules/@vitest/expect": {
"version": "4.1.3",
"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.3.tgz",
@@ -1045,15 +1085,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/data-uri-to-buffer": {
- "version": "4.0.1",
- "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz",
- "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==",
- "license": "MIT",
- "engines": {
- "node": ">= 12"
- }
- },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1153,29 +1184,6 @@
}
}
},
- "node_modules/fetch-blob": {
- "version": "3.2.0",
- "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz",
- "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "paypal",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "node-domexception": "^1.0.0",
- "web-streams-polyfill": "^3.0.3"
- },
- "engines": {
- "node": "^12.20 || >= 14.13"
- }
- },
"node_modules/fflate": {
"version": "0.8.2",
"resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.2.tgz",
@@ -1190,18 +1198,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/formdata-polyfill": {
- "version": "4.0.10",
- "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz",
- "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==",
- "license": "MIT",
- "dependencies": {
- "fetch-blob": "^3.1.2"
- },
- "engines": {
- "node": ">=12.20.0"
- }
- },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -1517,44 +1513,6 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
}
},
- "node_modules/node-domexception": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
- "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==",
- "deprecated": "Use your platform's native DOMException instead",
- "funding": [
- {
- "type": "github",
- "url": "https://github.com/sponsors/jimmywarting"
- },
- {
- "type": "github",
- "url": "https://paypal.me/jimmywarting"
- }
- ],
- "license": "MIT",
- "engines": {
- "node": ">=10.5.0"
- }
- },
- "node_modules/node-fetch": {
- "version": "3.3.2",
- "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz",
- "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==",
- "license": "MIT",
- "dependencies": {
- "data-uri-to-buffer": "^4.0.0",
- "fetch-blob": "^3.1.4",
- "formdata-polyfill": "^4.0.10"
- },
- "engines": {
- "node": "^12.20.0 || ^14.13.1 || >=16.0.0"
- },
- "funding": {
- "type": "opencollective",
- "url": "https://opencollective.com/node-fetch"
- }
- },
"node_modules/obug": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz",
@@ -1953,15 +1911,6 @@
}
}
},
- "node_modules/web-streams-polyfill": {
- "version": "3.3.3",
- "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz",
- "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==",
- "license": "MIT",
- "engines": {
- "node": ">= 8"
- }
- },
"node_modules/why-is-node-running": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz",
diff --git a/hindsight-integrations/openclaw/package.json b/hindsight-integrations/openclaw/package.json
index b89f17cd..6aa2c241 100644
--- a/hindsight-integrations/openclaw/package.json
+++ b/hindsight-integrations/openclaw/package.json
@@ -44,7 +44,8 @@
"prepublishOnly": "npm run clean && npm run build"
},
"dependencies": {
- "node-fetch": "^3.3.2"
+ "@vectorize-io/hindsight-client": "file:../../hindsight-clients/typescript",
+ "@vectorize-io/hindsight-all": "file:../../hindsight-all-npm"
},
"devDependencies": {
"@types/node": "^20.0.0",
diff --git a/hindsight-integrations/openclaw/src/backfill.test.ts b/hindsight-integrations/openclaw/src/backfill.test.ts
index 841a611b..5049ba66 100644
--- a/hindsight-integrations/openclaw/src/backfill.test.ts
+++ b/hindsight-integrations/openclaw/src/backfill.test.ts
@@ -10,13 +10,17 @@ const managerStart = vi.fn();
const managerStop = vi.fn();
const managerGetBaseUrl = vi.fn(() => 'http://127.0.0.1:9077');
-vi.mock('./embed-manager.js', () => ({
- HindsightEmbedManager: vi.fn(class {
- start = managerStart;
- stop = managerStop;
- getBaseUrl = managerGetBaseUrl;
- }),
-}));
+vi.mock('@vectorize-io/hindsight-all', async () => {
+ const actual = await vi.importActual('@vectorize-io/hindsight-all');
+ return {
+ ...actual,
+ HindsightServer: vi.fn(class {
+ start = managerStart;
+ stop = managerStop;
+ getBaseUrl = managerGetBaseUrl;
+ }),
+ };
+});
afterEach(() => {
vi.restoreAllMocks();
diff --git a/hindsight-integrations/openclaw/src/backfill.ts b/hindsight-integrations/openclaw/src/backfill.ts
index 02c87650..6bdf2cce 100644
--- a/hindsight-integrations/openclaw/src/backfill.ts
+++ b/hindsight-integrations/openclaw/src/backfill.ts
@@ -2,9 +2,9 @@
import { existsSync, realpathSync } from 'fs';
import { join, resolve } from 'path';
import { fileURLToPath, pathToFileURL } from 'url';
-import { HindsightEmbedManager } from './embed-manager.js';
-import { HindsightClient } from './client.js';
-import { buildClientOptions, detectExternalApi, detectLLMConfig } from './index.js';
+import { HindsightServer } from '@vectorize-io/hindsight-all';
+import { HindsightClient } from '@vectorize-io/hindsight-client';
+import { detectExternalApi, detectLLMConfig } from './index.js';
import type { BankStats, PluginConfig } from './types.js';
import {
buildBackfillPlan,
@@ -44,7 +44,7 @@ interface BackfillRuntime {
}
interface BankRuntime {
- client: HindsightClient;
+ bankId: string;
touchedEntryKeys: string[];
initialFailedOperations: number;
missionApplied: boolean;
@@ -297,16 +297,19 @@ export async function createBackfillRuntime(
}
const llmConfig = detectLLMConfig(pluginConfig);
- const manager = new HindsightEmbedManager(
- pluginConfig.apiPort || 9077,
- llmConfig.provider || '',
- llmConfig.apiKey || '',
- llmConfig.model,
- llmConfig.baseUrl,
- pluginConfig.daemonIdleTimeout ?? 0,
- pluginConfig.embedVersion,
- pluginConfig.embedPackagePath,
- );
+ const manager = new HindsightServer({
+ profile: 'openclaw',
+ port: pluginConfig.apiPort || 9077,
+ embedVersion: pluginConfig.embedVersion,
+ embedPackagePath: pluginConfig.embedPackagePath,
+ env: {
+ HINDSIGHT_API_LLM_PROVIDER: llmConfig.provider || '',
+ HINDSIGHT_API_LLM_API_KEY: llmConfig.apiKey || '',
+ HINDSIGHT_API_LLM_MODEL: llmConfig.model,
+ HINDSIGHT_API_LLM_BASE_URL: llmConfig.baseUrl,
+ HINDSIGHT_EMBED_DAEMON_IDLE_TIMEOUT: String(pluginConfig.daemonIdleTimeout ?? 0),
+ },
+ });
await manager.start();
return {
@@ -318,10 +321,29 @@ export async function createBackfillRuntime(
};
}
-async function waitForBankQueue(client: HindsightClient, maxPendingOperations: number): Promise {
+/**
+ * Fetch stats for a single bank over HTTP. The high-level `HindsightClient`
+ * doesn't yet wrap this endpoint, so we go direct — it's one call.
+ */
+async function fetchBankStats(baseUrl: string, apiToken: string | undefined, bankId: string): Promise {
+ const headers: Record = {};
+ if (apiToken) headers.Authorization = `Bearer ${apiToken}`;
+ const res = await fetch(`${baseUrl}/v1/default/banks/${encodeURIComponent(bankId)}/stats`, { headers });
+ if (!res.ok) {
+ throw new Error(`HTTP ${res.status}: ${await res.text().catch(() => '')}`);
+ }
+ return res.json() as Promise;
+}
+
+async function waitForBankQueue(
+ apiUrl: string,
+ apiToken: string | undefined,
+ bankId: string,
+ maxPendingOperations: number,
+): Promise {
for (;;) {
try {
- const stats = await client.getBankStats();
+ const stats = await fetchBankStats(apiUrl, apiToken, bankId);
if (stats.pending_operations <= maxPendingOperations) {
return;
}
@@ -335,9 +357,13 @@ async function waitForBankQueue(client: HindsightClient, maxPendingOperations: n
}
}
-async function getInitialBankStats(client: HindsightClient): Promise {
+async function getInitialBankStats(
+ apiUrl: string,
+ apiToken: string | undefined,
+ bankId: string,
+): Promise {
try {
- return await client.getBankStats();
+ return await fetchBankStats(apiUrl, apiToken, bankId);
} catch (error) {
if (error instanceof Error && error.message.includes('HTTP 404')) {
return null;
@@ -346,10 +372,15 @@ async function getInitialBankStats(client: HindsightClient): Promise): Promise