opencode-hindsight-plus 0.3.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 ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 best-linux-code
4
+ Copyright (c) Vectorize (upstream @vectorize-io/opencode-hindsight)
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,193 @@
1
+ # opencode-hindsight-plus
2
+
3
+ Hindsight memory plugin for [OpenCode](https://opencode.ai) — persistent long-term memory with **Claude Code-aligned per-turn auto-recall**.
4
+
5
+ Fork of [`@vectorize-io/opencode-hindsight`](https://github.com/vectorize-io/hindsight/tree/main/hindsight-integrations/opencode) with per-user-turn memory injection (see `SOURCE.txt`).
6
+
7
+ ## Features
8
+
9
+ - **Custom tools**: `hindsight_retain`, `hindsight_recall`, `hindsight_reflect` — the agent calls these explicitly
10
+ - **Per-turn auto-recall**: On every user message, queries Hindsight with the current prompt (Claude Code `UserPromptSubmit` alignment) and injects `<hindsight_memories>` into the system prompt. Tool-loop model calls re-use the same turn's cache (no extra API call).
11
+ - **Auto-retain**: Captures conversation on `session.idle` (Claude Code `Stop` alignment) and stores to Hindsight
12
+ - **Compaction hook**: Retains + injects query-relevant memories during context compaction so they survive window trimming
13
+
14
+ ## Quick Start
15
+
16
+ The plugin defaults to **Hindsight Cloud** (`https://api.hindsight.vectorize.io`). Just enable it and provide your API key.
17
+
18
+ ### 1. Enable the plugin
19
+
20
+ Add to your `opencode.json` (project) or `~/.config/opencode/opencode.json` (global):
21
+
22
+ ```json
23
+ {
24
+ "$schema": "https://opencode.ai/config.json",
25
+ "plugin": ["opencode-hindsight-plus"]
26
+ }
27
+ ```
28
+
29
+ OpenCode auto-installs plugins listed here on startup — no `npm install` required.
30
+
31
+ ### 2. Provide your Hindsight Cloud API key
32
+
33
+ Get an API key at [ui.hindsight.vectorize.io/connect](https://ui.hindsight.vectorize.io/connect), then:
34
+
35
+ ```bash
36
+ export HINDSIGHT_API_TOKEN="your-api-key"
37
+
38
+ # Optional: override the memory bank ID (defaults to "opencode")
39
+ export HINDSIGHT_BANK_ID="my-project"
40
+ ```
41
+
42
+ That's it — the plugin now reads/writes against your Cloud bank.
43
+
44
+ ### Using a self-hosted Hindsight instance
45
+
46
+ Point `HINDSIGHT_API_URL` at your server (the API key is then optional):
47
+
48
+ ```bash
49
+ export HINDSIGHT_API_URL="http://localhost:8888"
50
+ ```
51
+
52
+ Or configure inline in `opencode.json`:
53
+
54
+ ```json
55
+ {
56
+ "$schema": "https://opencode.ai/config.json",
57
+ "plugin": [
58
+ [
59
+ "opencode-hindsight-plus",
60
+ {
61
+ "hindsightApiUrl": "http://localhost:8888"
62
+ }
63
+ ]
64
+ ]
65
+ }
66
+ ```
67
+
68
+ ## Configuration
69
+
70
+ ### Plugin Options
71
+
72
+ Pass options directly in `opencode.json`:
73
+
74
+ ```json
75
+ {
76
+ "plugin": [
77
+ [
78
+ "opencode-hindsight-plus",
79
+ {
80
+ "hindsightApiUrl": "http://localhost:8888",
81
+ "bankId": "my-project",
82
+ "autoRecall": true,
83
+ "autoRetain": true,
84
+ "recallBudget": "mid"
85
+ }
86
+ ]
87
+ ]
88
+ }
89
+ ```
90
+
91
+ ### Config File
92
+
93
+ Create `~/.hindsight/opencode.json` for persistent configuration:
94
+
95
+ ```json
96
+ {
97
+ "hindsightApiUrl": "http://localhost:8888",
98
+ "hindsightApiToken": "your-api-key",
99
+ "recallBudget": "mid",
100
+ "retainEveryNTurns": 3,
101
+ "debug": false
102
+ }
103
+ ```
104
+
105
+ ### Environment Variables
106
+
107
+ | Variable | Description | Default |
108
+ | ----------------------------- | -------------------------------------------------------- | ------------------------------------- |
109
+ | `HINDSIGHT_API_URL` | Hindsight API base URL | `https://api.hindsight.vectorize.io` |
110
+ | `HINDSIGHT_API_TOKEN` | API key for authentication | (none — required for Hindsight Cloud) |
111
+ | `HINDSIGHT_BANK_ID` | Static memory bank ID | `opencode` |
112
+ | `HINDSIGHT_AGENT_NAME` | Agent name for dynamic bank IDs | `opencode` |
113
+ | `HINDSIGHT_AUTO_RECALL` | Auto-recall on every user turn | `true` |
114
+ | `HINDSIGHT_AUTO_RETAIN` | Auto-retain on session idle | `true` |
115
+ | `HINDSIGHT_RETAIN_MODE` | `full-session` or `last-turn` | `full-session` |
116
+ | `HINDSIGHT_RECALL_BUDGET` | Recall budget: `low`, `mid`, `high` | `mid` |
117
+ | `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens for recall results | `1024` |
118
+ | `HINDSIGHT_MIN_RECALL_PROMPT_CHARS` | Skip auto-recall when user prompt is shorter | `5` |
119
+ | `HINDSIGHT_RECALL_TAGS` | Comma-separated, filter recalls | (none) |
120
+ | `HINDSIGHT_RECALL_TAGS_MATCH` | Tag match mode: `any`, `all`, `any_strict`, `all_strict` | `any` |
121
+ | `HINDSIGHT_RETAIN_TAGS` | Comma-separated, added to every retain | (none) |
122
+ | `HINDSIGHT_DYNAMIC_BANK_ID` | Enable dynamic bank ID derivation | `false` |
123
+ | `HINDSIGHT_BANK_MISSION` | Bank mission/context | (none) |
124
+
125
+ > **Debug logging** is a config-only option (`"debug": true` in `opencode.json`
126
+ > plugin options or `~/.hindsight/opencode.json`) — there is intentionally no
127
+ > `HINDSIGHT_DEBUG` env var, because environment variables are unreliable to set
128
+ > for OpenCode's plugin runtime (notably on Windows). Errors and the resolved
129
+ > API URL/bank are logged regardless of this setting; `debug` only adds verbose
130
+ > tracing. All plugin logs go to OpenCode's log stream (`service=hindsight`),
131
+ > visible with `--print-logs` or in the OpenCode log files.
132
+
133
+ ### Configuration Priority
134
+
135
+ Settings are loaded in this order (later wins):
136
+
137
+ 1. Built-in defaults
138
+ 2. `~/.hindsight/opencode.json`
139
+ 3. Plugin options from `opencode.json`
140
+ 4. Environment variables
141
+
142
+ ## Tools
143
+
144
+ ### `hindsight_retain`
145
+
146
+ Store information in long-term memory. The agent uses this to save important facts, user preferences, project context, and decisions.
147
+
148
+ ### `hindsight_recall`
149
+
150
+ Search long-term memory. The agent uses this proactively before answering questions where prior context would help.
151
+
152
+ ### `hindsight_reflect`
153
+
154
+ Generate a synthesized answer from long-term memory. Unlike recall (raw memories), reflect produces a coherent summary.
155
+
156
+ ## Dynamic Bank IDs
157
+
158
+ For multi-project setups, enable dynamic bank ID derivation:
159
+
160
+ ```bash
161
+ export HINDSIGHT_DYNAMIC_BANK_ID=true
162
+ ```
163
+
164
+ The bank ID is composed from granularity fields (default: `agent::project`). Supported fields: `agent`, `project`, `gitProject`, `channel`, `user`.
165
+
166
+ - `project` uses the working directory basename. With this field, separate git worktrees of the same repository end up with different bank IDs because their paths differ.
167
+ - `gitProject` resolves to the main worktree's basename via `git rev-parse --git-common-dir`, so all linked worktrees of the same repository share a single bank. Falls back to the working directory basename when git is unavailable or the directory is not a repo. Use this in place of `project` if you want worktrees to share memory:
168
+
169
+ ```json
170
+ {
171
+ "dynamicBankId": true,
172
+ "dynamicBankGranularity": ["agent", "gitProject"]
173
+ }
174
+ ```
175
+
176
+ **Note:** The bank ID is derived once when the plugin loads, from environment variables set before OpenCode starts. These dimensions are process-scoped — they don't change per session within a running OpenCode process. For per-user isolation, set the env vars before launching each user's OpenCode instance:
177
+
178
+ ```bash
179
+ export HINDSIGHT_CHANNEL_ID="slack-general"
180
+ export HINDSIGHT_USER_ID="user123"
181
+ ```
182
+
183
+ ## Development
184
+
185
+ ```bash
186
+ npm install
187
+ npm test # Run tests
188
+ npm run build # Build to dist/
189
+ ```
190
+
191
+ ## License
192
+
193
+ MIT
@@ -0,0 +1,81 @@
1
+ import { Plugin } from '@opencode-ai/plugin';
2
+
3
+ interface HindsightConfig {
4
+ autoRecall: boolean;
5
+ recallBudget: string;
6
+ recallMaxTokens: number;
7
+ recallTypes: string[];
8
+ recallContextTurns: number;
9
+ recallMaxQueryChars: number;
10
+ /** Skip auto-recall when the latest user prompt is shorter than this (Claude Code: 5). */
11
+ minRecallPromptChars: number;
12
+ recallPromptPreamble: string;
13
+ recallTags: string[];
14
+ recallTagsMatch: "any" | "all" | "any_strict" | "all_strict";
15
+ autoRetain: boolean;
16
+ retainMode: string;
17
+ retainEveryNTurns: number;
18
+ retainOverlapTurns: number;
19
+ retainContext: string;
20
+ retainTags: string[];
21
+ retainMetadata: Record<string, string>;
22
+ hindsightApiUrl: string | null;
23
+ hindsightApiToken: string | null;
24
+ bankId: string | null;
25
+ bankIdPrefix: string;
26
+ dynamicBankId: boolean;
27
+ dynamicBankGranularity: string[];
28
+ bankMission: string;
29
+ retainMission: string | null;
30
+ agentName: string;
31
+ debug: boolean;
32
+ }
33
+
34
+ /**
35
+ * Hook implementations for the Hindsight OpenCode plugin.
36
+ *
37
+ * Aligned with Claude Code hindsight-memory:
38
+ * - experimental.chat.system.transform → per-user-turn auto-recall
39
+ * (Claude Code: UserPromptSubmit + additionalContext)
40
+ * - event (session.idle) → auto-retain after the assistant finishes
41
+ * (Claude Code: Stop hook)
42
+ * - experimental.session.compacting → retain + inject for compaction
43
+ */
44
+
45
+ /** Cached recall for one user turn (reused across tool-loop system transforms). */
46
+ interface TurnRecallCache {
47
+ userTurnCount: number;
48
+ /** Formatted context, or null when API succeeded with zero results. */
49
+ context: string | null;
50
+ }
51
+ interface PluginState {
52
+ turnCount: number;
53
+ missionsSet: Set<string>;
54
+ /** Per-session last successful per-turn recall (Claude Code UserPromptSubmit). */
55
+ turnRecall: Map<string, TurnRecallCache>;
56
+ /** Track last retained turn count per session to avoid duplicates */
57
+ lastRetainedTurn: Map<string, number>;
58
+ }
59
+
60
+ /**
61
+ * Hindsight OpenCode Plugin — persistent long-term memory for OpenCode agents.
62
+ *
63
+ * Provides:
64
+ * - Custom tools: hindsight_retain, hindsight_recall, hindsight_reflect
65
+ * - Per-user-turn auto-recall (Claude Code UserPromptSubmit alignment)
66
+ * - Auto-retain on session.idle (Claude Code Stop alignment)
67
+ * - Memory preservation during context compaction
68
+ *
69
+ * @example
70
+ * ```json
71
+ * // opencode.json
72
+ * { "plugin": ["opencode-hindsight-plus"] }
73
+ *
74
+ * // With options:
75
+ * { "plugin": [["opencode-hindsight-plus", { "bankId": "my-bank" }]] }
76
+ * ```
77
+ */
78
+
79
+ declare const HindsightPlugin: Plugin;
80
+
81
+ export { type HindsightConfig, HindsightPlugin, type PluginState, HindsightPlugin as default };
package/dist/index.js ADDED
@@ -0,0 +1,719 @@
1
+ // src/index.ts
2
+ import { HindsightClient } from "@vectorize-io/hindsight-client";
3
+
4
+ // src/config.ts
5
+ import { readFileSync } from "fs";
6
+ import { join } from "path";
7
+ import { homedir } from "os";
8
+ var DEFAULT_HINDSIGHT_API_URL = "https://api.hindsight.vectorize.io";
9
+ var DEFAULTS = {
10
+ // Recall — defaults aligned with Claude Code hindsight-memory plugin
11
+ autoRecall: true,
12
+ recallBudget: "mid",
13
+ recallMaxTokens: 1024,
14
+ // Observations are consolidated beliefs; reduces duplicate raw facts per turn.
15
+ recallTypes: ["observation"],
16
+ recallContextTurns: 1,
17
+ recallMaxQueryChars: 800,
18
+ minRecallPromptChars: 5,
19
+ recallTags: [],
20
+ recallTagsMatch: "any",
21
+ recallPromptPreamble: "Relevant memories from past conversations (prioritize recent when conflicting). Only use memories that are directly useful to continue this conversation; ignore the rest:",
22
+ // Retain — defaults aligned with Claude Code (Stop every N turns)
23
+ autoRetain: true,
24
+ retainMode: "full-session",
25
+ retainEveryNTurns: 10,
26
+ retainOverlapTurns: 2,
27
+ retainContext: "opencode",
28
+ retainTags: [],
29
+ retainMetadata: {},
30
+ // Connection
31
+ hindsightApiUrl: DEFAULT_HINDSIGHT_API_URL,
32
+ hindsightApiToken: null,
33
+ // Bank
34
+ bankId: null,
35
+ bankIdPrefix: "",
36
+ dynamicBankId: false,
37
+ dynamicBankGranularity: ["agent", "project"],
38
+ bankMission: "",
39
+ retainMission: null,
40
+ agentName: "opencode",
41
+ // Misc
42
+ debug: false
43
+ };
44
+ var ENV_OVERRIDES = {
45
+ HINDSIGHT_API_URL: ["hindsightApiUrl", "string"],
46
+ HINDSIGHT_API_TOKEN: ["hindsightApiToken", "string"],
47
+ HINDSIGHT_BANK_ID: ["bankId", "string"],
48
+ HINDSIGHT_AGENT_NAME: ["agentName", "string"],
49
+ HINDSIGHT_AUTO_RECALL: ["autoRecall", "bool"],
50
+ HINDSIGHT_AUTO_RETAIN: ["autoRetain", "bool"],
51
+ HINDSIGHT_RETAIN_MODE: ["retainMode", "string"],
52
+ HINDSIGHT_RECALL_BUDGET: ["recallBudget", "string"],
53
+ HINDSIGHT_RECALL_MAX_TOKENS: ["recallMaxTokens", "int"],
54
+ HINDSIGHT_RECALL_MAX_QUERY_CHARS: ["recallMaxQueryChars", "int"],
55
+ HINDSIGHT_RECALL_CONTEXT_TURNS: ["recallContextTurns", "int"],
56
+ HINDSIGHT_MIN_RECALL_PROMPT_CHARS: ["minRecallPromptChars", "int"],
57
+ HINDSIGHT_DYNAMIC_BANK_ID: ["dynamicBankId", "bool"],
58
+ HINDSIGHT_BANK_MISSION: ["bankMission", "string"],
59
+ HINDSIGHT_BANK_ID_PREFIX: ["bankIdPrefix", "string"],
60
+ HINDSIGHT_RETAIN_EVERY_N_TURNS: ["retainEveryNTurns", "int"],
61
+ HINDSIGHT_RETAIN_OVERLAP_TURNS: ["retainOverlapTurns", "int"],
62
+ HINDSIGHT_RECALL_TAGS: ["recallTags", "string"],
63
+ HINDSIGHT_RETAIN_TAGS: ["retainTags", "string"],
64
+ HINDSIGHT_RECALL_TAGS_MATCH: ["recallTagsMatch", "string"],
65
+ HINDSIGHT_RECALL_PROMPT_PREAMBLE: ["recallPromptPreamble", "string"],
66
+ HINDSIGHT_RETAIN_CONTEXT: ["retainContext", "string"]
67
+ // NOTE: `debug` is intentionally NOT an env override. It is a proper config
68
+ // option set via opencode.json plugin options or ~/.hindsight/opencode.json,
69
+ // because env vars are unreliable to set for OpenCode's plugin runtime
70
+ // (notably on Windows).
71
+ };
72
+ function castEnv(value, typ) {
73
+ if (typ === "bool") return ["true", "1", "yes"].includes(value.toLowerCase());
74
+ if (typ === "int") {
75
+ const n = parseInt(value, 10);
76
+ return isNaN(n) ? null : n;
77
+ }
78
+ return value;
79
+ }
80
+ function loadSettingsFile(path) {
81
+ try {
82
+ const raw = readFileSync(path, "utf-8");
83
+ return JSON.parse(raw);
84
+ } catch {
85
+ return {};
86
+ }
87
+ }
88
+ function loadConfig(pluginOptions) {
89
+ const config = { ...DEFAULTS };
90
+ const userConfigPath = join(homedir(), ".hindsight", "opencode.json");
91
+ const fileConfig = loadSettingsFile(userConfigPath);
92
+ for (const [key, value] of Object.entries(fileConfig)) {
93
+ if (value !== null && value !== void 0) {
94
+ config[key] = value;
95
+ }
96
+ }
97
+ if (pluginOptions) {
98
+ for (const [key, value] of Object.entries(pluginOptions)) {
99
+ if (value !== null && value !== void 0) {
100
+ config[key] = value;
101
+ }
102
+ }
103
+ }
104
+ for (const [envName, [key, typ]] of Object.entries(ENV_OVERRIDES)) {
105
+ const val = process.env[envName];
106
+ if (val !== void 0) {
107
+ const castVal = castEnv(val, typ);
108
+ if (castVal !== null) {
109
+ config[key] = castVal;
110
+ }
111
+ }
112
+ }
113
+ const recallTagsEnv = process.env["HINDSIGHT_RECALL_TAGS"];
114
+ if (recallTagsEnv !== void 0) {
115
+ config["recallTags"] = recallTagsEnv.split(",").map((t) => t.trim()).filter(Boolean);
116
+ }
117
+ const recallTagsMatchEnv = process.env["HINDSIGHT_RECALL_TAGS_MATCH"];
118
+ if (recallTagsMatchEnv !== void 0) {
119
+ config["recallTagsMatch"] = recallTagsMatchEnv;
120
+ }
121
+ const retainTagsEnv = process.env["HINDSIGHT_RETAIN_TAGS"];
122
+ if (retainTagsEnv !== void 0) {
123
+ config["retainTags"] = retainTagsEnv.split(",").map((t) => t.trim()).filter(Boolean);
124
+ }
125
+ const result = config;
126
+ const VALID_RETAIN_MODES = ["full-session", "last-turn"];
127
+ if (!VALID_RETAIN_MODES.includes(result.retainMode)) {
128
+ console.error(
129
+ `[Hindsight] Unknown retainMode "${result.retainMode}" \u2014 valid: ${VALID_RETAIN_MODES.join(", ")}. Falling back to "full-session".`
130
+ );
131
+ result.retainMode = "full-session";
132
+ }
133
+ const VALID_TAGS_MATCH = ["any", "all", "any_strict", "all_strict"];
134
+ if (!VALID_TAGS_MATCH.includes(result.recallTagsMatch)) {
135
+ console.error(
136
+ `[Hindsight] Unknown recallTagsMatch "${result.recallTagsMatch}" \u2014 valid: ${VALID_TAGS_MATCH.join(", ")}. Falling back to "any".`
137
+ );
138
+ result.recallTagsMatch = "any";
139
+ }
140
+ const VALID_BUDGETS = ["low", "mid", "high"];
141
+ if (!VALID_BUDGETS.includes(result.recallBudget)) {
142
+ console.error(
143
+ `[Hindsight] Unknown recallBudget "${result.recallBudget}" \u2014 valid: ${VALID_BUDGETS.join(", ")}. Falling back to "mid".`
144
+ );
145
+ result.recallBudget = "mid";
146
+ }
147
+ return result;
148
+ }
149
+
150
+ // src/bank.ts
151
+ import { basename, dirname } from "path";
152
+ import { execFileSync } from "child_process";
153
+
154
+ // src/logger.ts
155
+ var SERVICE = "hindsight";
156
+ var Logger = class {
157
+ client;
158
+ debugEnabled;
159
+ silent;
160
+ constructor(options = {}) {
161
+ this.client = options.client;
162
+ this.debugEnabled = options.debug ?? false;
163
+ this.silent = options.silent ?? false;
164
+ }
165
+ emit(level, message, extra) {
166
+ if (this.silent) return;
167
+ const app = this.client?.app;
168
+ if (app && typeof app.log === "function") {
169
+ try {
170
+ const result = app.log({ body: { service: SERVICE, level, message, extra } });
171
+ if (result && typeof result.then === "function") {
172
+ result.then(void 0, () => {
173
+ });
174
+ }
175
+ return;
176
+ } catch {
177
+ }
178
+ }
179
+ const line = extra ? `[Hindsight] ${message} ${JSON.stringify(extra)}` : `[Hindsight] ${message}`;
180
+ console.error(line);
181
+ }
182
+ error(message, error) {
183
+ this.emit("error", message, error === void 0 ? void 0 : { error: errorToString(error) });
184
+ }
185
+ warn(message, extra) {
186
+ this.emit("warn", message, extra);
187
+ }
188
+ info(message, extra) {
189
+ this.emit("info", message, extra);
190
+ }
191
+ debug(message, extra) {
192
+ if (this.debugEnabled) this.emit("debug", message, extra);
193
+ }
194
+ };
195
+ function errorToString(error) {
196
+ if (error instanceof Error) return error.stack || `${error.name}: ${error.message}`;
197
+ return String(error);
198
+ }
199
+
200
+ // src/bank.ts
201
+ var DEFAULT_BANK_NAME = "opencode";
202
+ var VALID_FIELDS = /* @__PURE__ */ new Set(["agent", "project", "gitProject", "channel", "user"]);
203
+ function getProjectRootFromGit(directory) {
204
+ if (!directory) return null;
205
+ try {
206
+ const commonDir = execFileSync(
207
+ "git",
208
+ ["rev-parse", "--path-format=absolute", "--git-common-dir"],
209
+ {
210
+ cwd: directory,
211
+ encoding: "utf-8",
212
+ stdio: ["ignore", "pipe", "ignore"],
213
+ timeout: 1e3
214
+ }
215
+ ).trim();
216
+ if (!commonDir) return null;
217
+ if (basename(commonDir) === ".git") {
218
+ return dirname(commonDir);
219
+ }
220
+ return commonDir;
221
+ } catch {
222
+ return null;
223
+ }
224
+ }
225
+ function deriveGitProjectName(directory) {
226
+ const projectRoot = getProjectRootFromGit(directory);
227
+ if (projectRoot) return basename(projectRoot);
228
+ return directory ? basename(directory) : "unknown";
229
+ }
230
+ function deriveBankId(config, directory) {
231
+ const prefix = config.bankIdPrefix;
232
+ if (!config.dynamicBankId) {
233
+ const base = config.bankId || DEFAULT_BANK_NAME;
234
+ return prefix ? `${prefix}-${base}` : base;
235
+ }
236
+ const fields = config.dynamicBankGranularity?.length ? config.dynamicBankGranularity : ["agent", "project"];
237
+ for (const f of fields) {
238
+ if (!VALID_FIELDS.has(f)) {
239
+ console.error(
240
+ `[Hindsight] Unknown dynamicBankGranularity field "${f}" \u2014 valid: ${[...VALID_FIELDS].sort().join(", ")}`
241
+ );
242
+ }
243
+ }
244
+ const channelId = process.env.HINDSIGHT_CHANNEL_ID || "";
245
+ const userId = process.env.HINDSIGHT_USER_ID || "";
246
+ const fieldResolvers = {
247
+ agent: () => config.agentName || "opencode",
248
+ project: () => directory ? basename(directory) : "unknown",
249
+ gitProject: () => deriveGitProjectName(directory),
250
+ channel: () => channelId || "default",
251
+ user: () => userId || "anonymous"
252
+ };
253
+ const segments = fields.map((f) => fieldResolvers[f]?.() || "unknown");
254
+ const baseBankId = segments.join("::");
255
+ return prefix ? `${prefix}-${baseBankId}` : baseBankId;
256
+ }
257
+ async function ensureBankMission(client, bankId, config, missionsSet, logger = new Logger({ silent: true })) {
258
+ const mission = config.bankMission;
259
+ if (!mission?.trim()) return;
260
+ if (missionsSet.has(bankId)) return;
261
+ try {
262
+ await client.createBank(bankId, {
263
+ reflectMission: mission,
264
+ retainMission: config.retainMission || void 0
265
+ });
266
+ missionsSet.add(bankId);
267
+ if (missionsSet.size > 1e4) {
268
+ const keys = [...missionsSet].sort();
269
+ for (const k of keys.slice(0, keys.length >> 1)) {
270
+ missionsSet.delete(k);
271
+ }
272
+ }
273
+ logger.debug(`Set mission for bank: ${bankId}`);
274
+ } catch (e) {
275
+ logger.debug(`Could not set bank mission for ${bankId}`, { error: String(e) });
276
+ }
277
+ }
278
+
279
+ // src/tools.ts
280
+ import { tool } from "@opencode-ai/plugin/tool";
281
+
282
+ // src/content.ts
283
+ function stripMemoryTags(content) {
284
+ content = content.replace(/<hindsight_memories>[\s\S]*?<\/hindsight_memories>/g, "");
285
+ content = content.replace(/<relevant_memories>[\s\S]*?<\/relevant_memories>/g, "");
286
+ return content;
287
+ }
288
+ function injectHindsightMemories(systemSection, context) {
289
+ const base = stripMemoryTags(systemSection).replace(/\n{3,}/g, "\n\n").trimEnd();
290
+ return base ? `${base}
291
+
292
+ ${context}` : context;
293
+ }
294
+ function formatMemories(results) {
295
+ if (!results.length) return "";
296
+ return results.map((r) => {
297
+ const typeStr = r.type ? ` [${r.type}]` : "";
298
+ const dateStr = r.mentioned_at ? ` (${r.mentioned_at})` : "";
299
+ return `- ${r.text}${typeStr}${dateStr}`;
300
+ }).join("\n\n");
301
+ }
302
+ function formatCurrentTime() {
303
+ const now = /* @__PURE__ */ new Date();
304
+ const y = now.getUTCFullYear();
305
+ const m = String(now.getUTCMonth() + 1).padStart(2, "0");
306
+ const d = String(now.getUTCDate()).padStart(2, "0");
307
+ const h = String(now.getUTCHours()).padStart(2, "0");
308
+ const min = String(now.getUTCMinutes()).padStart(2, "0");
309
+ return `${y}-${m}-${d} ${h}:${min}`;
310
+ }
311
+ function composeRecallQuery(latestQuery, messages, recallContextTurns) {
312
+ const latest = latestQuery.trim();
313
+ if (recallContextTurns <= 1 || !messages.length) return latest;
314
+ const contextual = sliceLastTurnsByUserBoundary(messages, recallContextTurns);
315
+ const contextLines = [];
316
+ for (const msg of contextual) {
317
+ const content = stripMemoryTags(msg.content).trim();
318
+ if (!content) continue;
319
+ if (msg.role === "user" && content === latest) continue;
320
+ contextLines.push(`${msg.role}: ${content}`);
321
+ }
322
+ if (!contextLines.length) return latest;
323
+ return ["Prior context:", contextLines.join("\n"), latest].join("\n\n");
324
+ }
325
+ function truncateRecallQuery(query, latestQuery, maxChars) {
326
+ if (maxChars <= 0 || query.length <= maxChars) return query;
327
+ const latest = latestQuery.trim();
328
+ const latestOnly = latest.length > maxChars ? latest.slice(0, maxChars) : latest;
329
+ if (!query.includes("Prior context:")) return latestOnly;
330
+ const contextMarker = "Prior context:\n\n";
331
+ const markerIndex = query.indexOf(contextMarker);
332
+ if (markerIndex === -1) return latestOnly;
333
+ const suffix = "\n\n" + latest;
334
+ const suffixIndex = query.lastIndexOf(suffix);
335
+ if (suffixIndex === -1) return latestOnly;
336
+ if (suffix.length >= maxChars) return latestOnly;
337
+ const contextBody = query.slice(markerIndex + contextMarker.length, suffixIndex);
338
+ const contextLines = contextBody.split("\n").filter(Boolean);
339
+ const kept = [];
340
+ for (let i = contextLines.length - 1; i >= 0; i--) {
341
+ kept.unshift(contextLines[i]);
342
+ const candidate = `${contextMarker}${kept.join("\n")}${suffix}`;
343
+ if (candidate.length > maxChars) {
344
+ kept.shift();
345
+ break;
346
+ }
347
+ }
348
+ if (kept.length) return `${contextMarker}${kept.join("\n")}${suffix}`;
349
+ return latestOnly;
350
+ }
351
+ function sliceLastTurnsByUserBoundary(messages, turns) {
352
+ if (!messages.length || turns <= 0) return [];
353
+ let userTurnsSeen = 0;
354
+ let startIndex = -1;
355
+ for (let i = messages.length - 1; i >= 0; i--) {
356
+ if (messages[i].role === "user") {
357
+ userTurnsSeen++;
358
+ if (userTurnsSeen >= turns) {
359
+ startIndex = i;
360
+ break;
361
+ }
362
+ }
363
+ }
364
+ return startIndex === -1 ? [...messages] : messages.slice(startIndex);
365
+ }
366
+ function prepareRetentionTranscript(messages, retainFullWindow = false) {
367
+ if (!messages.length) return { transcript: null, messageCount: 0 };
368
+ let targetMessages;
369
+ if (retainFullWindow) {
370
+ targetMessages = messages;
371
+ } else {
372
+ let lastUserIdx = -1;
373
+ for (let i = messages.length - 1; i >= 0; i--) {
374
+ if (messages[i].role === "user") {
375
+ lastUserIdx = i;
376
+ break;
377
+ }
378
+ }
379
+ if (lastUserIdx === -1) return { transcript: null, messageCount: 0 };
380
+ targetMessages = messages.slice(lastUserIdx);
381
+ }
382
+ const parts = [];
383
+ for (const msg of targetMessages) {
384
+ const content = stripMemoryTags(msg.content).trim();
385
+ if (!content) continue;
386
+ parts.push(`[role: ${msg.role}]
387
+ ${content}
388
+ [${msg.role}:end]`);
389
+ }
390
+ if (!parts.length) return { transcript: null, messageCount: 0 };
391
+ const transcript = parts.join("\n\n");
392
+ if (transcript.trim().length < 10) return { transcript: null, messageCount: 0 };
393
+ return { transcript, messageCount: parts.length };
394
+ }
395
+
396
+ // src/tools.ts
397
+ function createTools(client, bankId, config, missionsSet, logger = new Logger({ silent: true })) {
398
+ const hindsight_retain = tool({
399
+ description: "Store information in long-term memory. Use this to remember important facts, user preferences, project context, decisions, and anything worth recalling in future sessions. Be specific \u2014 include who, what, when, and why.",
400
+ args: {
401
+ content: tool.schema.string().describe("The information to remember. Be specific and self-contained."),
402
+ context: tool.schema.string().optional().describe("Optional context about where this information came from.")
403
+ },
404
+ async execute(args) {
405
+ if (missionsSet) {
406
+ await ensureBankMission(client, bankId, config, missionsSet, logger);
407
+ }
408
+ await client.retain(bankId, args.content, {
409
+ context: args.context || config.retainContext,
410
+ tags: config.retainTags.length ? config.retainTags : void 0,
411
+ metadata: Object.keys(config.retainMetadata).length ? config.retainMetadata : void 0
412
+ });
413
+ return "Memory stored successfully.";
414
+ }
415
+ });
416
+ const hindsight_recall = tool({
417
+ description: "Search long-term memory for relevant information. Use this proactively before answering questions about past conversations, user preferences, project history, or any topic where prior context would help. When in doubt, recall first.",
418
+ args: {
419
+ query: tool.schema.string().describe("Natural language search query. Be specific about what you need to know.")
420
+ },
421
+ async execute(args) {
422
+ const response = await client.recall(bankId, args.query, {
423
+ budget: config.recallBudget,
424
+ maxTokens: config.recallMaxTokens,
425
+ types: config.recallTypes,
426
+ tags: config.recallTags.length ? config.recallTags : void 0,
427
+ tagsMatch: config.recallTags.length ? config.recallTagsMatch : void 0
428
+ });
429
+ const results = response.results || [];
430
+ if (!results.length) return "No relevant memories found.";
431
+ const formatted = formatMemories(results);
432
+ return `Found ${results.length} relevant memories (as of ${formatCurrentTime()} UTC):
433
+
434
+ ${formatted}`;
435
+ }
436
+ });
437
+ const hindsight_reflect = tool({
438
+ description: 'Generate a thoughtful answer using long-term memory. Unlike recall (which returns raw memories), reflect synthesizes memories into a coherent answer. Use for questions like "What do you know about this user?" or "Summarize our project decisions."',
439
+ args: {
440
+ query: tool.schema.string().describe("The question to answer using long-term memory."),
441
+ context: tool.schema.string().optional().describe("Optional additional context to guide the reflection.")
442
+ },
443
+ async execute(args) {
444
+ if (missionsSet) {
445
+ await ensureBankMission(client, bankId, config, missionsSet, logger);
446
+ }
447
+ const response = await client.reflect(bankId, args.query, {
448
+ context: args.context,
449
+ budget: config.recallBudget
450
+ });
451
+ return response.text || "No relevant information found to reflect on.";
452
+ }
453
+ });
454
+ return { hindsight_retain, hindsight_recall, hindsight_reflect };
455
+ }
456
+
457
+ // src/hooks.ts
458
+ function countUserTurns(messages) {
459
+ return messages.filter((m) => m.role === "user").length;
460
+ }
461
+ function lastUserMessage(messages) {
462
+ for (let i = messages.length - 1; i >= 0; i--) {
463
+ if (messages[i].role === "user") return messages[i];
464
+ }
465
+ return void 0;
466
+ }
467
+ function applyContextToSystem(output, context) {
468
+ output.system[0] = injectHindsightMemories(output.system[0] ?? "", context);
469
+ }
470
+ function capMapSize(map, max) {
471
+ if (map.size <= max) return;
472
+ const first = map.keys().next().value;
473
+ if (first !== void 0) map.delete(first);
474
+ }
475
+ function createHooks(hindsightClient, bankId, config, state2, opencodeClient, logger = new Logger({ silent: true })) {
476
+ async function recallForContext(query) {
477
+ try {
478
+ const response = await hindsightClient.recall(bankId, query, {
479
+ budget: config.recallBudget,
480
+ maxTokens: config.recallMaxTokens,
481
+ types: config.recallTypes,
482
+ tags: config.recallTags.length ? config.recallTags : void 0,
483
+ tagsMatch: config.recallTags.length ? config.recallTagsMatch : void 0
484
+ });
485
+ const results = response.results || [];
486
+ if (!results.length) return { context: null, ok: true };
487
+ const formatted = formatMemories(results);
488
+ const context = `<hindsight_memories>
489
+ ${config.recallPromptPreamble}
490
+ Current time: ${formatCurrentTime()} UTC
491
+
492
+ ${formatted}
493
+ </hindsight_memories>`;
494
+ return { context, ok: true };
495
+ } catch (e) {
496
+ logger.error("Recall failed", e);
497
+ return { context: null, ok: false };
498
+ }
499
+ }
500
+ async function getSessionMessages(sessionId) {
501
+ try {
502
+ logger.debug(`getSessionMessages: fetching messages for session ${sessionId}`);
503
+ const response = await opencodeClient.session.messages({
504
+ path: { id: sessionId }
505
+ });
506
+ if (response.error) {
507
+ logger.warn("getSessionMessages: OpenCode returned an error", {
508
+ error: JSON.stringify(response.error)?.substring(0, 500)
509
+ });
510
+ }
511
+ const rawMessages = response.data || [];
512
+ const messages = [];
513
+ for (const msg of rawMessages) {
514
+ const role = msg.info.role;
515
+ if (role !== "user" && role !== "assistant") continue;
516
+ const textParts = msg.parts.filter((p) => p.type === "text" && p.text).map((p) => p.text);
517
+ if (textParts.length) {
518
+ messages.push({ role, content: textParts.join("\n") });
519
+ }
520
+ }
521
+ logger.debug(`getSessionMessages: raw=${rawMessages.length}, parsed=${messages.length}`);
522
+ return messages;
523
+ } catch (e) {
524
+ logger.error("Failed to get session messages", e);
525
+ return [];
526
+ }
527
+ }
528
+ async function retainSession(sessionId, messages) {
529
+ const retainFullWindow = config.retainMode === "full-session";
530
+ let targetMessages;
531
+ let documentId;
532
+ if (retainFullWindow) {
533
+ targetMessages = messages;
534
+ documentId = sessionId;
535
+ } else {
536
+ const windowTurns = config.retainEveryNTurns + config.retainOverlapTurns;
537
+ targetMessages = sliceLastTurnsByUserBoundary(messages, windowTurns);
538
+ documentId = `${sessionId}-${Date.now()}`;
539
+ }
540
+ const { transcript } = prepareRetentionTranscript(targetMessages, true);
541
+ if (!transcript) return;
542
+ await ensureBankMission(hindsightClient, bankId, config, state2.missionsSet, logger);
543
+ await hindsightClient.retain(bankId, transcript, {
544
+ documentId,
545
+ context: config.retainContext,
546
+ tags: config.retainTags.length ? config.retainTags : void 0,
547
+ metadata: Object.keys(config.retainMetadata).length ? { ...config.retainMetadata, session_id: sessionId } : { session_id: sessionId },
548
+ async: true
549
+ });
550
+ }
551
+ async function handleSessionIdle(sessionId) {
552
+ logger.debug(`handleSessionIdle called for session ${sessionId}`);
553
+ if (!config.autoRetain) return;
554
+ const messages = await getSessionMessages(sessionId);
555
+ if (!messages.length) return;
556
+ const userTurns = countUserTurns(messages);
557
+ const lastRetained = state2.lastRetainedTurn.get(sessionId) || 0;
558
+ logger.debug(
559
+ `handleSessionIdle: userTurns=${userTurns}, lastRetained=${lastRetained}, retainEveryNTurns=${config.retainEveryNTurns}`
560
+ );
561
+ if (userTurns - lastRetained < config.retainEveryNTurns) return;
562
+ try {
563
+ await retainSession(sessionId, messages);
564
+ state2.lastRetainedTurn.set(sessionId, userTurns);
565
+ logger.info(`Auto-retained ${messages.length} messages`, {
566
+ session: sessionId,
567
+ bank: bankId
568
+ });
569
+ } catch (e) {
570
+ logger.error("Auto-retain failed", e);
571
+ }
572
+ }
573
+ const event = async (input) => {
574
+ try {
575
+ const { event: evt } = input;
576
+ logger.debug(`event hook fired: type=${evt.type}`);
577
+ if (evt.type === "session.idle") {
578
+ const sessionId = evt.properties.sessionID;
579
+ if (sessionId) {
580
+ await handleSessionIdle(sessionId);
581
+ }
582
+ }
583
+ } catch (e) {
584
+ logger.error("Event hook error", e);
585
+ }
586
+ };
587
+ const compacting = async (input, output) => {
588
+ try {
589
+ const messages = await getSessionMessages(input.sessionID);
590
+ if (messages.length && config.autoRetain) {
591
+ try {
592
+ await retainSession(input.sessionID, messages);
593
+ state2.lastRetainedTurn.delete(input.sessionID);
594
+ logger.debug("Pre-compaction retain completed");
595
+ } catch (e) {
596
+ logger.error("Pre-compaction retain failed", e);
597
+ }
598
+ }
599
+ if (messages.length) {
600
+ const lastUserMsg = lastUserMessage(messages);
601
+ if (lastUserMsg) {
602
+ const query = composeRecallQuery(
603
+ lastUserMsg.content,
604
+ messages,
605
+ config.recallContextTurns
606
+ );
607
+ const truncated = truncateRecallQuery(
608
+ query,
609
+ lastUserMsg.content,
610
+ config.recallMaxQueryChars
611
+ );
612
+ const { context } = await recallForContext(truncated);
613
+ if (context) {
614
+ output.context.push(context);
615
+ }
616
+ }
617
+ }
618
+ } catch (e) {
619
+ logger.error("Compaction hook error", e);
620
+ }
621
+ };
622
+ const systemTransform = async (input, output) => {
623
+ try {
624
+ if (!config.autoRecall) return;
625
+ const sessionId = input.sessionID;
626
+ if (!sessionId) return;
627
+ const messages = await getSessionMessages(sessionId);
628
+ const lastUser = lastUserMessage(messages);
629
+ if (!lastUser) {
630
+ logger.debug(`systemTransform: no user message yet for ${sessionId}`);
631
+ return;
632
+ }
633
+ const prompt = lastUser.content.trim();
634
+ if (prompt.length < config.minRecallPromptChars) {
635
+ logger.debug(
636
+ `systemTransform: prompt too short (${prompt.length} < ${config.minRecallPromptChars}), skip`
637
+ );
638
+ return;
639
+ }
640
+ const userTurns = countUserTurns(messages);
641
+ const cached = state2.turnRecall.get(sessionId);
642
+ if (cached && cached.userTurnCount === userTurns) {
643
+ if (cached.context) {
644
+ applyContextToSystem(output, cached.context);
645
+ logger.debug(`Re-injected cached recall for session ${sessionId} turn ${userTurns}`);
646
+ }
647
+ return;
648
+ }
649
+ await ensureBankMission(hindsightClient, bankId, config, state2.missionsSet, logger);
650
+ const query = composeRecallQuery(prompt, messages, config.recallContextTurns);
651
+ const truncated = truncateRecallQuery(query, prompt, config.recallMaxQueryChars);
652
+ logger.debug(
653
+ `systemTransform: recalling for turn ${userTurns}, queryLen=${truncated.length}`
654
+ );
655
+ const { context, ok } = await recallForContext(truncated);
656
+ if (ok) {
657
+ state2.turnRecall.set(sessionId, { userTurnCount: userTurns, context });
658
+ capMapSize(state2.turnRecall, 1e3);
659
+ }
660
+ if (context) {
661
+ applyContextToSystem(output, context);
662
+ logger.debug(`Injected per-turn recall for session ${sessionId} turn ${userTurns}`);
663
+ }
664
+ } catch (e) {
665
+ logger.error("System transform hook error", e);
666
+ }
667
+ };
668
+ return {
669
+ event,
670
+ "experimental.session.compacting": compacting,
671
+ "experimental.chat.system.transform": systemTransform
672
+ };
673
+ }
674
+
675
+ // src/index.ts
676
+ var state = {
677
+ turnCount: 0,
678
+ missionsSet: /* @__PURE__ */ new Set(),
679
+ turnRecall: /* @__PURE__ */ new Map(),
680
+ lastRetainedTurn: /* @__PURE__ */ new Map()
681
+ };
682
+ var HindsightPlugin = async (input, options) => {
683
+ const config = loadConfig(options);
684
+ const logger = new Logger({
685
+ client: input.client,
686
+ debug: config.debug
687
+ });
688
+ const client = new HindsightClient({
689
+ baseUrl: config.hindsightApiUrl,
690
+ apiKey: config.hindsightApiToken || void 0
691
+ });
692
+ const bankId = deriveBankId(config, input.directory);
693
+ logger.info("Hindsight plugin initialized", {
694
+ api: config.hindsightApiUrl,
695
+ bank: bankId,
696
+ authenticated: Boolean(config.hindsightApiToken),
697
+ autoRecall: config.autoRecall,
698
+ autoRetain: config.autoRetain
699
+ });
700
+ const tools = createTools(client, bankId, config, state.missionsSet, logger);
701
+ const hooks = createHooks(
702
+ client,
703
+ bankId,
704
+ config,
705
+ state,
706
+ input.client,
707
+ logger
708
+ );
709
+ return {
710
+ tool: tools,
711
+ ...hooks
712
+ };
713
+ };
714
+ var index_default = HindsightPlugin;
715
+ export {
716
+ HindsightPlugin,
717
+ index_default as default
718
+ };
719
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/config.ts","../src/bank.ts","../src/logger.ts","../src/tools.ts","../src/content.ts","../src/hooks.ts"],"sourcesContent":["/**\n * Hindsight OpenCode Plugin — persistent long-term memory for OpenCode agents.\n *\n * Provides:\n * - Custom tools: hindsight_retain, hindsight_recall, hindsight_reflect\n * - Per-user-turn auto-recall (Claude Code UserPromptSubmit alignment)\n * - Auto-retain on session.idle (Claude Code Stop alignment)\n * - Memory preservation during context compaction\n *\n * @example\n * ```json\n * // opencode.json\n * { \"plugin\": [\"opencode-hindsight-plus\"] }\n *\n * // With options:\n * { \"plugin\": [[\"opencode-hindsight-plus\", { \"bankId\": \"my-bank\" }]] }\n * ```\n */\n\nimport type { Plugin } from \"@opencode-ai/plugin\";\nimport { HindsightClient } from \"@vectorize-io/hindsight-client\";\nimport { loadConfig } from \"./config.js\";\nimport { deriveBankId } from \"./bank.js\";\nimport { createTools } from \"./tools.js\";\nimport { createHooks, type PluginState } from \"./hooks.js\";\nimport { Logger, type OpencodeLogClient } from \"./logger.js\";\n\n// Module-level state persists across sessions (plugin is instantiated per session,\n// but the module is loaded once per OpenCode server process).\nconst state: PluginState = {\n turnCount: 0,\n missionsSet: new Set(),\n turnRecall: new Map(),\n lastRetainedTurn: new Map(),\n};\n\nconst HindsightPlugin: Plugin = async (input, options) => {\n const config = loadConfig(options);\n\n // Route logs through OpenCode's server log stream (TUI-safe). error/warn/info\n // are always emitted; debug is gated on config.debug.\n const logger = new Logger({\n client: input.client as unknown as OpencodeLogClient,\n debug: config.debug,\n });\n\n // hindsightApiUrl always resolves to a value (DEFAULT_HINDSIGHT_API_URL by default),\n // so plugin instantiation never fails just because the URL is unset.\n // Requests fail at call time if no API key is configured for a Cloud URL —\n // that surfaces a clear, actionable error from the server rather than silently\n // disabling the plugin.\n const client = new HindsightClient({\n baseUrl: config.hindsightApiUrl!,\n apiKey: config.hindsightApiToken || undefined,\n });\n\n const bankId = deriveBankId(config, input.directory);\n // Always log the resolved endpoint + bank so users can see which instance the\n // plugin is talking to (a common source of \"memories aren't saving\" confusion).\n logger.info(\"Hindsight plugin initialized\", {\n api: config.hindsightApiUrl,\n bank: bankId,\n authenticated: Boolean(config.hindsightApiToken),\n autoRecall: config.autoRecall,\n autoRetain: config.autoRetain,\n });\n\n const tools = createTools(client, bankId, config, state.missionsSet, logger);\n const hooks = createHooks(\n client,\n bankId,\n config,\n state,\n input.client as unknown as Parameters<typeof createHooks>[4],\n logger\n );\n\n return {\n tool: tools,\n ...hooks,\n };\n};\n\n// Named export for direct import\nexport { HindsightPlugin };\n\n// Default export is the Plugin function itself — OpenCode's legacy loader\n// iterates Object.values(mod) and calls every function export as a Plugin\n// factory. Keep the entry surface free of utility re-exports (loadConfig,\n// deriveBankId) so they are not invoked as plugins. The plugin itself imports\n// those utilities directly from their modules.\nexport default HindsightPlugin;\n\n// Re-export types for consumers\nexport type { HindsightConfig } from \"./config.js\";\nexport type { PluginState } from \"./hooks.js\";\n","/**\n * Configuration management for the Hindsight OpenCode plugin.\n *\n * Loading order (later entries win):\n * 1. Built-in defaults\n * 2. User config file (~/.hindsight/opencode.json)\n * 3. Plugin options (from opencode.json plugin tuple)\n * 4. Environment variable overrides\n */\n\nimport { readFileSync } from \"node:fs\";\nimport { join } from \"node:path\";\nimport { homedir } from \"node:os\";\n\n/** Default API URL used when no override is supplied via env, file, or plugin options. */\nexport const DEFAULT_HINDSIGHT_API_URL = \"https://api.hindsight.vectorize.io\";\n\nexport interface HindsightConfig {\n // Recall\n autoRecall: boolean;\n recallBudget: string;\n recallMaxTokens: number;\n recallTypes: string[];\n recallContextTurns: number;\n recallMaxQueryChars: number;\n /** Skip auto-recall when the latest user prompt is shorter than this (Claude Code: 5). */\n minRecallPromptChars: number;\n recallPromptPreamble: string;\n recallTags: string[];\n recallTagsMatch: \"any\" | \"all\" | \"any_strict\" | \"all_strict\";\n\n // Retain\n autoRetain: boolean;\n retainMode: string;\n retainEveryNTurns: number;\n retainOverlapTurns: number;\n retainContext: string;\n retainTags: string[];\n retainMetadata: Record<string, string>;\n\n // Connection\n hindsightApiUrl: string | null;\n hindsightApiToken: string | null;\n\n // Bank\n bankId: string | null;\n bankIdPrefix: string;\n dynamicBankId: boolean;\n dynamicBankGranularity: string[];\n bankMission: string;\n retainMission: string | null;\n agentName: string;\n\n // Misc\n debug: boolean;\n}\n\nconst DEFAULTS: HindsightConfig = {\n // Recall — defaults aligned with Claude Code hindsight-memory plugin\n autoRecall: true,\n recallBudget: \"mid\",\n recallMaxTokens: 1024,\n // Observations are consolidated beliefs; reduces duplicate raw facts per turn.\n recallTypes: [\"observation\"],\n recallContextTurns: 1,\n recallMaxQueryChars: 800,\n minRecallPromptChars: 5,\n recallTags: [],\n recallTagsMatch: \"any\",\n recallPromptPreamble:\n \"Relevant memories from past conversations (prioritize recent when \" +\n \"conflicting). Only use memories that are directly useful to continue \" +\n \"this conversation; ignore the rest:\",\n\n // Retain — defaults aligned with Claude Code (Stop every N turns)\n autoRetain: true,\n retainMode: \"full-session\",\n retainEveryNTurns: 10,\n retainOverlapTurns: 2,\n retainContext: \"opencode\",\n retainTags: [],\n retainMetadata: {},\n\n // Connection\n hindsightApiUrl: DEFAULT_HINDSIGHT_API_URL,\n hindsightApiToken: null,\n\n // Bank\n bankId: null,\n bankIdPrefix: \"\",\n dynamicBankId: false,\n dynamicBankGranularity: [\"agent\", \"project\"],\n bankMission: \"\",\n retainMission: null,\n agentName: \"opencode\",\n\n // Misc\n debug: false,\n};\n\n/** Env var → config key + type mapping */\nconst ENV_OVERRIDES: Record<string, [keyof HindsightConfig, \"string\" | \"bool\" | \"int\"]> = {\n HINDSIGHT_API_URL: [\"hindsightApiUrl\", \"string\"],\n HINDSIGHT_API_TOKEN: [\"hindsightApiToken\", \"string\"],\n HINDSIGHT_BANK_ID: [\"bankId\", \"string\"],\n HINDSIGHT_AGENT_NAME: [\"agentName\", \"string\"],\n HINDSIGHT_AUTO_RECALL: [\"autoRecall\", \"bool\"],\n HINDSIGHT_AUTO_RETAIN: [\"autoRetain\", \"bool\"],\n HINDSIGHT_RETAIN_MODE: [\"retainMode\", \"string\"],\n HINDSIGHT_RECALL_BUDGET: [\"recallBudget\", \"string\"],\n HINDSIGHT_RECALL_MAX_TOKENS: [\"recallMaxTokens\", \"int\"],\n HINDSIGHT_RECALL_MAX_QUERY_CHARS: [\"recallMaxQueryChars\", \"int\"],\n HINDSIGHT_RECALL_CONTEXT_TURNS: [\"recallContextTurns\", \"int\"],\n HINDSIGHT_MIN_RECALL_PROMPT_CHARS: [\"minRecallPromptChars\", \"int\"],\n HINDSIGHT_DYNAMIC_BANK_ID: [\"dynamicBankId\", \"bool\"],\n HINDSIGHT_BANK_MISSION: [\"bankMission\", \"string\"],\n HINDSIGHT_BANK_ID_PREFIX: [\"bankIdPrefix\", \"string\"],\n HINDSIGHT_RETAIN_EVERY_N_TURNS: [\"retainEveryNTurns\", \"int\"],\n HINDSIGHT_RETAIN_OVERLAP_TURNS: [\"retainOverlapTurns\", \"int\"],\n HINDSIGHT_RECALL_TAGS: [\"recallTags\", \"string\"],\n HINDSIGHT_RETAIN_TAGS: [\"retainTags\", \"string\"],\n HINDSIGHT_RECALL_TAGS_MATCH: [\"recallTagsMatch\", \"string\"],\n HINDSIGHT_RECALL_PROMPT_PREAMBLE: [\"recallPromptPreamble\", \"string\"],\n HINDSIGHT_RETAIN_CONTEXT: [\"retainContext\", \"string\"],\n // NOTE: `debug` is intentionally NOT an env override. It is a proper config\n // option set via opencode.json plugin options or ~/.hindsight/opencode.json,\n // because env vars are unreliable to set for OpenCode's plugin runtime\n // (notably on Windows).\n};\n\nfunction castEnv(value: string, typ: \"string\" | \"bool\" | \"int\"): string | boolean | number | null {\n if (typ === \"bool\") return [\"true\", \"1\", \"yes\"].includes(value.toLowerCase());\n if (typ === \"int\") {\n const n = parseInt(value, 10);\n return isNaN(n) ? null : n;\n }\n return value;\n}\n\nfunction loadSettingsFile(path: string): Record<string, unknown> {\n try {\n const raw = readFileSync(path, \"utf-8\");\n return JSON.parse(raw);\n } catch {\n return {};\n }\n}\n\nexport function loadConfig(pluginOptions?: Record<string, unknown>): HindsightConfig {\n // 1. Start with defaults\n const config: Record<string, unknown> = { ...DEFAULTS };\n\n // 2. User config file (~/.hindsight/opencode.json)\n const userConfigPath = join(homedir(), \".hindsight\", \"opencode.json\");\n const fileConfig = loadSettingsFile(userConfigPath);\n for (const [key, value] of Object.entries(fileConfig)) {\n if (value !== null && value !== undefined) {\n config[key] = value;\n }\n }\n\n // 3. Plugin options (from opencode.json: [\"@vectorize-io/opencode-hindsight\", { ... }])\n if (pluginOptions) {\n for (const [key, value] of Object.entries(pluginOptions)) {\n if (value !== null && value !== undefined) {\n config[key] = value;\n }\n }\n }\n\n // 4. Environment variable overrides (highest priority)\n for (const [envName, [key, typ]] of Object.entries(ENV_OVERRIDES)) {\n const val = process.env[envName];\n if (val !== undefined) {\n const castVal = castEnv(val, typ);\n if (castVal !== null) {\n config[key] = castVal;\n }\n }\n }\n\n // Array env vars (comma-separated)\n const recallTagsEnv = process.env[\"HINDSIGHT_RECALL_TAGS\"];\n if (recallTagsEnv !== undefined) {\n config[\"recallTags\"] = recallTagsEnv\n .split(\",\")\n .map((t) => t.trim())\n .filter(Boolean);\n }\n const recallTagsMatchEnv = process.env[\"HINDSIGHT_RECALL_TAGS_MATCH\"];\n if (recallTagsMatchEnv !== undefined) {\n config[\"recallTagsMatch\"] = recallTagsMatchEnv;\n }\n\n const retainTagsEnv = process.env[\"HINDSIGHT_RETAIN_TAGS\"];\n if (retainTagsEnv !== undefined) {\n config[\"retainTags\"] = retainTagsEnv\n .split(\",\")\n .map((t) => t.trim())\n .filter(Boolean);\n }\n\n const result = config as unknown as HindsightConfig;\n\n // Validate enum-like fields to catch typos early\n const VALID_RETAIN_MODES = [\"full-session\", \"last-turn\"];\n if (!VALID_RETAIN_MODES.includes(result.retainMode)) {\n console.error(\n `[Hindsight] Unknown retainMode \"${result.retainMode}\" — ` +\n `valid: ${VALID_RETAIN_MODES.join(\", \")}. Falling back to \"full-session\".`\n );\n result.retainMode = \"full-session\";\n }\n\n const VALID_TAGS_MATCH = [\"any\", \"all\", \"any_strict\", \"all_strict\"];\n if (!VALID_TAGS_MATCH.includes(result.recallTagsMatch)) {\n console.error(\n `[Hindsight] Unknown recallTagsMatch \"${result.recallTagsMatch}\" — ` +\n `valid: ${VALID_TAGS_MATCH.join(\", \")}. Falling back to \"any\".`\n );\n result.recallTagsMatch = \"any\";\n }\n\n const VALID_BUDGETS = [\"low\", \"mid\", \"high\"];\n if (!VALID_BUDGETS.includes(result.recallBudget)) {\n console.error(\n `[Hindsight] Unknown recallBudget \"${result.recallBudget}\" — ` +\n `valid: ${VALID_BUDGETS.join(\", \")}. Falling back to \"mid\".`\n );\n result.recallBudget = \"mid\";\n }\n\n return result;\n}\n","/**\n * Bank ID derivation and mission management.\n *\n * Port of Claude Code plugin's bank.py, adapted for OpenCode's context model.\n *\n * Dimensions for dynamic bank IDs:\n * - agent → configured name or \"opencode\"\n * - project → derived from the working directory basename\n * - gitProject → derived from the main worktree's basename when inside a\n * git repository (so all linked worktrees of the same repo\n * share a single memory bank). Falls back to the working\n * directory basename when git is unavailable or the\n * directory is not a repo.\n */\n\nimport { basename, dirname } from \"node:path\";\nimport { execFileSync } from \"node:child_process\";\nimport type { HindsightConfig } from \"./config.js\";\nimport { Logger } from \"./logger.js\";\nimport type { HindsightClient } from \"@vectorize-io/hindsight-client\";\n\nconst DEFAULT_BANK_NAME = \"opencode\";\nconst VALID_FIELDS = new Set([\"agent\", \"project\", \"gitProject\", \"channel\", \"user\"]);\n\n/**\n * Resolve the main worktree root for a directory inside a git repository.\n *\n * Uses `git rev-parse --path-format=absolute --git-common-dir`, which always\n * points to the .git directory of the *main* worktree, even when invoked from\n * a linked worktree (created with `git worktree add`). The parent of that path\n * is the main worktree root, so all linked worktrees of the same repo resolve\n * to the same root and end up sharing one memory bank.\n *\n * Returns `null` when git is unavailable, the directory is not a repo, or the\n * git invocation fails for any other reason.\n */\nfunction getProjectRootFromGit(directory: string): string | null {\n if (!directory) return null;\n try {\n const commonDir = execFileSync(\n \"git\",\n [\"rev-parse\", \"--path-format=absolute\", \"--git-common-dir\"],\n {\n cwd: directory,\n encoding: \"utf-8\",\n stdio: [\"ignore\", \"pipe\", \"ignore\"],\n timeout: 1000,\n }\n ).trim();\n if (!commonDir) return null;\n // For typical clones and `git worktree add`, common-dir is `<root>/.git`,\n // so the parent is the main worktree root. For bare repos, common-dir is\n // the bare directory itself (e.g. `myrepo.git`); use it directly.\n if (basename(commonDir) === \".git\") {\n return dirname(commonDir);\n }\n return commonDir;\n } catch {\n return null;\n }\n}\n\nfunction deriveGitProjectName(directory: string): string {\n const projectRoot = getProjectRootFromGit(directory);\n if (projectRoot) return basename(projectRoot);\n return directory ? basename(directory) : \"unknown\";\n}\n\n/**\n * Derive a bank ID from context and config.\n *\n * Static mode: returns config.bankId or DEFAULT_BANK_NAME.\n * Dynamic mode: composes from granularity fields joined by '::'.\n */\nexport function deriveBankId(config: HindsightConfig, directory: string): string {\n const prefix = config.bankIdPrefix;\n\n if (!config.dynamicBankId) {\n const base = config.bankId || DEFAULT_BANK_NAME;\n return prefix ? `${prefix}-${base}` : base;\n }\n\n const fields = config.dynamicBankGranularity?.length\n ? config.dynamicBankGranularity\n : [\"agent\", \"project\"];\n\n for (const f of fields) {\n if (!VALID_FIELDS.has(f)) {\n console.error(\n `[Hindsight] Unknown dynamicBankGranularity field \"${f}\" — ` +\n `valid: ${[...VALID_FIELDS].sort().join(\", \")}`\n );\n }\n }\n\n const channelId = process.env.HINDSIGHT_CHANNEL_ID || \"\";\n const userId = process.env.HINDSIGHT_USER_ID || \"\";\n\n // Lazy resolution so we don't spawn `git` for `gitProject` when the field\n // isn't part of the configured granularity.\n const fieldResolvers: Record<string, () => string> = {\n agent: () => config.agentName || \"opencode\",\n project: () => (directory ? basename(directory) : \"unknown\"),\n gitProject: () => deriveGitProjectName(directory),\n channel: () => channelId || \"default\",\n user: () => userId || \"anonymous\",\n };\n\n // bank_id is stored as-is server-side; HTTP path encoding is the client layer's job.\n const segments = fields.map((f) => fieldResolvers[f]?.() || \"unknown\");\n const baseBankId = segments.join(\"::\");\n\n return prefix ? `${prefix}-${baseBankId}` : baseBankId;\n}\n\n/**\n * Set bank mission on first use, skip if already set.\n * Uses an in-memory Set (plugin is long-lived, unlike Claude Code's ephemeral hooks).\n */\nexport async function ensureBankMission(\n client: HindsightClient,\n bankId: string,\n config: HindsightConfig,\n missionsSet: Set<string>,\n logger: Logger = new Logger({ silent: true })\n): Promise<void> {\n const mission = config.bankMission;\n if (!mission?.trim()) return;\n if (missionsSet.has(bankId)) return;\n\n try {\n await client.createBank(bankId, {\n reflectMission: mission,\n retainMission: config.retainMission || undefined,\n });\n missionsSet.add(bankId);\n // Cap tracked banks\n if (missionsSet.size > 10000) {\n const keys = [...missionsSet].sort();\n for (const k of keys.slice(0, keys.length >> 1)) {\n missionsSet.delete(k);\n }\n }\n logger.debug(`Set mission for bank: ${bankId}`);\n } catch (e) {\n // Don't fail if mission set fails — bank may not exist yet\n logger.debug(`Could not set bank mission for ${bankId}`, { error: String(e) });\n }\n}\n","/**\n * Lightweight logger for the Hindsight OpenCode plugin.\n *\n * Routes through OpenCode's server log API (`client.app.log`) when available so\n * messages land in OpenCode's own log stream — visible via `--print-logs` and\n * the OpenCode log files — without writing to stdout/stderr and corrupting the\n * TUI. Falls back to `console.error` when the OpenCode client is unavailable\n * (e.g. degraded load); unit tests use a silent logger.\n *\n * Levels:\n * - error / warn / info → always emitted, so failures and the resolved\n * endpoint are visible WITHOUT any debug opt-in.\n * - debug → emitted only when `debug` is enabled in config.\n */\n\nexport type LogLevel = \"debug\" | \"info\" | \"warn\" | \"error\";\n\n/** Minimal shape of OpenCode's `client.app.log` that we depend on. */\nexport interface OpencodeLogClient {\n app?: {\n log?: (options: {\n body: {\n service: string;\n level: LogLevel;\n message: string;\n extra?: Record<string, unknown>;\n };\n }) => unknown;\n };\n}\n\nexport interface LoggerOptions {\n /** OpenCode client used to write into the server log stream. */\n client?: OpencodeLogClient;\n /** When true, `debug()` messages are emitted. */\n debug?: boolean;\n /** When true, the logger emits nothing (used as a safe default in tests). */\n silent?: boolean;\n}\n\nconst SERVICE = \"hindsight\";\n\nexport class Logger {\n private readonly client?: OpencodeLogClient;\n private readonly debugEnabled: boolean;\n private readonly silent: boolean;\n\n constructor(options: LoggerOptions = {}) {\n this.client = options.client;\n this.debugEnabled = options.debug ?? false;\n this.silent = options.silent ?? false;\n }\n\n private emit(level: LogLevel, message: string, extra?: Record<string, unknown>): void {\n if (this.silent) return;\n\n const app = this.client?.app;\n if (app && typeof app.log === \"function\") {\n try {\n // IMPORTANT: call as a method on `app` so `this` is preserved.\n // OpenCode's `app.log` is a class method that uses `this` internally;\n // calling a detached reference (`const log = app.log; log(...)`) throws\n // `this._client is undefined`, which would otherwise be swallowed below\n // and produce no log at all.\n const result = app.log({ body: { service: SERVICE, level, message, extra } });\n // Fire-and-forget: a logging failure must never surface to OpenCode.\n if (result && typeof (result as Promise<unknown>).then === \"function\") {\n (result as Promise<unknown>).then(undefined, () => {});\n }\n return;\n } catch {\n // Fall through to the console fallback if app.log throws synchronously.\n }\n }\n\n // Fallback when no OpenCode client is available (or app.log failed).\n // OpenCode captures plugin console output into its logs, so this stays\n // visible and TUI-safe.\n const line = extra\n ? `[Hindsight] ${message} ${JSON.stringify(extra)}`\n : `[Hindsight] ${message}`;\n console.error(line);\n }\n\n error(message: string, error?: unknown): void {\n this.emit(\"error\", message, error === undefined ? undefined : { error: errorToString(error) });\n }\n\n warn(message: string, extra?: Record<string, unknown>): void {\n this.emit(\"warn\", message, extra);\n }\n\n info(message: string, extra?: Record<string, unknown>): void {\n this.emit(\"info\", message, extra);\n }\n\n debug(message: string, extra?: Record<string, unknown>): void {\n if (this.debugEnabled) this.emit(\"debug\", message, extra);\n }\n}\n\nfunction errorToString(error: unknown): string {\n if (error instanceof Error) return error.stack || `${error.name}: ${error.message}`;\n return String(error);\n}\n","/**\n * Custom tool definitions for the Hindsight OpenCode plugin.\n *\n * Registers hindsight_retain, hindsight_recall, and hindsight_reflect\n * as tools the agent can call explicitly.\n */\n\nimport { tool } from \"@opencode-ai/plugin/tool\";\nimport type { ToolDefinition } from \"@opencode-ai/plugin/tool\";\nimport type { HindsightClient } from \"@vectorize-io/hindsight-client\";\nimport type { HindsightConfig } from \"./config.js\";\nimport { formatMemories, formatCurrentTime } from \"./content.js\";\nimport { ensureBankMission } from \"./bank.js\";\nimport { Logger } from \"./logger.js\";\n\nexport interface HindsightTools {\n hindsight_retain: ToolDefinition;\n hindsight_recall: ToolDefinition;\n hindsight_reflect: ToolDefinition;\n // Index signature so the object is assignable to OpenCode's Hooks.tool\n // (Record<string, ToolDefinition>) without losing the specific keys above.\n [key: string]: ToolDefinition;\n}\n\nexport function createTools(\n client: HindsightClient,\n bankId: string,\n config: HindsightConfig,\n missionsSet?: Set<string>,\n logger: Logger = new Logger({ silent: true })\n): HindsightTools {\n const hindsight_retain = tool({\n description:\n \"Store information in long-term memory. Use this to remember important facts, \" +\n \"user preferences, project context, decisions, and anything worth recalling in future sessions. \" +\n \"Be specific — include who, what, when, and why.\",\n args: {\n content: tool.schema\n .string()\n .describe(\"The information to remember. Be specific and self-contained.\"),\n context: tool.schema\n .string()\n .optional()\n .describe(\"Optional context about where this information came from.\"),\n },\n async execute(args) {\n if (missionsSet) {\n await ensureBankMission(client, bankId, config, missionsSet, logger);\n }\n await client.retain(bankId, args.content, {\n context: args.context || config.retainContext,\n tags: config.retainTags.length ? config.retainTags : undefined,\n metadata: Object.keys(config.retainMetadata).length ? config.retainMetadata : undefined,\n });\n return \"Memory stored successfully.\";\n },\n });\n\n const hindsight_recall = tool({\n description:\n \"Search long-term memory for relevant information. Use this proactively before \" +\n \"answering questions about past conversations, user preferences, project history, \" +\n \"or any topic where prior context would help. When in doubt, recall first.\",\n args: {\n query: tool.schema\n .string()\n .describe(\"Natural language search query. Be specific about what you need to know.\"),\n },\n async execute(args) {\n const response = await client.recall(bankId, args.query, {\n budget: config.recallBudget as \"low\" | \"mid\" | \"high\",\n maxTokens: config.recallMaxTokens,\n types: config.recallTypes,\n tags: config.recallTags.length ? config.recallTags : undefined,\n tagsMatch: config.recallTags.length ? config.recallTagsMatch : undefined,\n });\n\n const results = response.results || [];\n if (!results.length) return \"No relevant memories found.\";\n\n const formatted = formatMemories(results);\n return `Found ${results.length} relevant memories (as of ${formatCurrentTime()} UTC):\\n\\n${formatted}`;\n },\n });\n\n const hindsight_reflect = tool({\n description:\n \"Generate a thoughtful answer using long-term memory. Unlike recall (which returns \" +\n \"raw memories), reflect synthesizes memories into a coherent answer. Use for questions \" +\n 'like \"What do you know about this user?\" or \"Summarize our project decisions.\"',\n args: {\n query: tool.schema.string().describe(\"The question to answer using long-term memory.\"),\n context: tool.schema\n .string()\n .optional()\n .describe(\"Optional additional context to guide the reflection.\"),\n },\n async execute(args) {\n if (missionsSet) {\n await ensureBankMission(client, bankId, config, missionsSet, logger);\n }\n const response = await client.reflect(bankId, args.query, {\n context: args.context,\n budget: config.recallBudget as \"low\" | \"mid\" | \"high\",\n });\n\n return response.text || \"No relevant information found to reflect on.\";\n },\n });\n\n return { hindsight_retain, hindsight_recall, hindsight_reflect };\n}\n","/**\n * Content processing utilities.\n *\n * Port of the Claude Code plugin's content.py:\n * - Memory tag stripping (anti-feedback-loop)\n * - Recall query composition and truncation\n * - Memory formatting for context injection\n * - Retention transcript formatting\n */\n\n/** Strip <hindsight_memories> and <relevant_memories> blocks to prevent retain feedback loops. */\nexport function stripMemoryTags(content: string): string {\n content = content.replace(/<hindsight_memories>[\\s\\S]*?<\\/hindsight_memories>/g, \"\");\n content = content.replace(/<relevant_memories>[\\s\\S]*?<\\/relevant_memories>/g, \"\");\n return content;\n}\n\n/**\n * Fold a fresh `<hindsight_memories>` block into a system section.\n * Replaces any previous block so per-turn recall does not stack forever.\n */\nexport function injectHindsightMemories(systemSection: string, context: string): string {\n const base = stripMemoryTags(systemSection)\n .replace(/\\n{3,}/g, \"\\n\\n\")\n .trimEnd();\n return base ? `${base}\\n\\n${context}` : context;\n}\n\nexport interface RecallResult {\n text: string;\n type?: string | null;\n mentioned_at?: string | null;\n}\n\n/** Format recall results into human-readable text for context injection. */\nexport function formatMemories(results: RecallResult[]): string {\n if (!results.length) return \"\";\n return results\n .map((r) => {\n const typeStr = r.type ? ` [${r.type}]` : \"\";\n const dateStr = r.mentioned_at ? ` (${r.mentioned_at})` : \"\";\n return `- ${r.text}${typeStr}${dateStr}`;\n })\n .join(\"\\n\\n\");\n}\n\n/** Format current UTC time for recall context. */\nexport function formatCurrentTime(): string {\n const now = new Date();\n const y = now.getUTCFullYear();\n const m = String(now.getUTCMonth() + 1).padStart(2, \"0\");\n const d = String(now.getUTCDate()).padStart(2, \"0\");\n const h = String(now.getUTCHours()).padStart(2, \"0\");\n const min = String(now.getUTCMinutes()).padStart(2, \"0\");\n return `${y}-${m}-${d} ${h}:${min}`;\n}\n\nexport interface Message {\n role: string;\n content: string;\n}\n\n/**\n * Compose a multi-turn recall query from conversation history.\n *\n * When recallContextTurns > 1, includes prior context above the latest query.\n */\nexport function composeRecallQuery(\n latestQuery: string,\n messages: Message[],\n recallContextTurns: number\n): string {\n const latest = latestQuery.trim();\n if (recallContextTurns <= 1 || !messages.length) return latest;\n\n const contextual = sliceLastTurnsByUserBoundary(messages, recallContextTurns);\n const contextLines: string[] = [];\n\n for (const msg of contextual) {\n const content = stripMemoryTags(msg.content).trim();\n if (!content) continue;\n if (msg.role === \"user\" && content === latest) continue;\n contextLines.push(`${msg.role}: ${content}`);\n }\n\n if (!contextLines.length) return latest;\n\n return [\"Prior context:\", contextLines.join(\"\\n\"), latest].join(\"\\n\\n\");\n}\n\n/**\n * Truncate a composed recall query to maxChars.\n * Preserves the latest user message, drops oldest context lines first.\n */\nexport function truncateRecallQuery(query: string, latestQuery: string, maxChars: number): string {\n if (maxChars <= 0 || query.length <= maxChars) return query;\n\n const latest = latestQuery.trim();\n const latestOnly = latest.length > maxChars ? latest.slice(0, maxChars) : latest;\n\n if (!query.includes(\"Prior context:\")) return latestOnly;\n\n const contextMarker = \"Prior context:\\n\\n\";\n const markerIndex = query.indexOf(contextMarker);\n if (markerIndex === -1) return latestOnly;\n\n const suffix = \"\\n\\n\" + latest;\n const suffixIndex = query.lastIndexOf(suffix);\n if (suffixIndex === -1) return latestOnly;\n if (suffix.length >= maxChars) return latestOnly;\n\n const contextBody = query.slice(markerIndex + contextMarker.length, suffixIndex);\n const contextLines = contextBody.split(\"\\n\").filter(Boolean);\n\n const kept: string[] = [];\n for (let i = contextLines.length - 1; i >= 0; i--) {\n kept.unshift(contextLines[i]);\n const candidate = `${contextMarker}${kept.join(\"\\n\")}${suffix}`;\n if (candidate.length > maxChars) {\n kept.shift();\n break;\n }\n }\n\n if (kept.length) return `${contextMarker}${kept.join(\"\\n\")}${suffix}`;\n return latestOnly;\n}\n\n/** Slice messages to the last N turns, where a turn starts at a user message. */\nexport function sliceLastTurnsByUserBoundary(messages: Message[], turns: number): Message[] {\n if (!messages.length || turns <= 0) return [];\n\n let userTurnsSeen = 0;\n let startIndex = -1;\n\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"user\") {\n userTurnsSeen++;\n if (userTurnsSeen >= turns) {\n startIndex = i;\n break;\n }\n }\n }\n\n return startIndex === -1 ? [...messages] : messages.slice(startIndex);\n}\n\n/**\n * Format messages into a retention transcript.\n *\n * Uses [role: ...]...[role:end] markers for structured retention.\n */\nexport function prepareRetentionTranscript(\n messages: Message[],\n retainFullWindow: boolean = false\n): { transcript: string | null; messageCount: number } {\n if (!messages.length) return { transcript: null, messageCount: 0 };\n\n let targetMessages: Message[];\n if (retainFullWindow) {\n targetMessages = messages;\n } else {\n // Default: retain only the last turn\n let lastUserIdx = -1;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"user\") {\n lastUserIdx = i;\n break;\n }\n }\n if (lastUserIdx === -1) return { transcript: null, messageCount: 0 };\n targetMessages = messages.slice(lastUserIdx);\n }\n\n const parts: string[] = [];\n for (const msg of targetMessages) {\n const content = stripMemoryTags(msg.content).trim();\n if (!content) continue;\n parts.push(`[role: ${msg.role}]\\n${content}\\n[${msg.role}:end]`);\n }\n\n if (!parts.length) return { transcript: null, messageCount: 0 };\n\n const transcript = parts.join(\"\\n\\n\");\n if (transcript.trim().length < 10) return { transcript: null, messageCount: 0 };\n\n return { transcript, messageCount: parts.length };\n}\n","/**\n * Hook implementations for the Hindsight OpenCode plugin.\n *\n * Aligned with Claude Code hindsight-memory:\n * - experimental.chat.system.transform → per-user-turn auto-recall\n * (Claude Code: UserPromptSubmit + additionalContext)\n * - event (session.idle) → auto-retain after the assistant finishes\n * (Claude Code: Stop hook)\n * - experimental.session.compacting → retain + inject for compaction\n */\n\nimport type { HindsightClient } from \"@vectorize-io/hindsight-client\";\nimport type { HindsightConfig } from \"./config.js\";\nimport { Logger } from \"./logger.js\";\nimport {\n formatMemories,\n formatCurrentTime,\n composeRecallQuery,\n truncateRecallQuery,\n prepareRetentionTranscript,\n sliceLastTurnsByUserBoundary,\n injectHindsightMemories,\n type Message,\n} from \"./content.js\";\nimport { ensureBankMission } from \"./bank.js\";\n\n/** Cached recall for one user turn (reused across tool-loop system transforms). */\nexport interface TurnRecallCache {\n userTurnCount: number;\n /** Formatted context, or null when API succeeded with zero results. */\n context: string | null;\n}\n\nexport interface PluginState {\n turnCount: number;\n missionsSet: Set<string>;\n /** Per-session last successful per-turn recall (Claude Code UserPromptSubmit). */\n turnRecall: Map<string, TurnRecallCache>;\n /** Track last retained turn count per session to avoid duplicates */\n lastRetainedTurn: Map<string, number>;\n}\n\ninterface EventInput {\n event: {\n type: string;\n properties: Record<string, unknown>;\n };\n}\n\ninterface CompactingInput {\n sessionID: string;\n}\n\ninterface CompactingOutput {\n context: string[];\n prompt?: string;\n}\n\ninterface SystemTransformInput {\n sessionID?: string;\n model: unknown;\n}\n\ninterface SystemTransformOutput {\n system: string[];\n}\n\ntype OpencodeClient = {\n session: {\n messages: (params: { path: { id: string } }) => Promise<{\n data?: Array<{\n info: { role: string };\n parts: Array<{ type: string; text?: string }>;\n }>;\n error?: unknown;\n request?: unknown;\n response?: unknown;\n }>;\n };\n};\n\nexport interface HindsightHooks {\n event: (input: EventInput) => Promise<void>;\n \"experimental.session.compacting\": (\n input: CompactingInput,\n output: CompactingOutput\n ) => Promise<void>;\n \"experimental.chat.system.transform\": (\n input: SystemTransformInput,\n output: SystemTransformOutput\n ) => Promise<void>;\n}\n\nfunction countUserTurns(messages: readonly Message[]): number {\n return messages.filter((m) => m.role === \"user\").length;\n}\n\nfunction lastUserMessage(messages: readonly Message[]): Message | undefined {\n for (let i = messages.length - 1; i >= 0; i--) {\n if (messages[i].role === \"user\") return messages[i];\n }\n return undefined;\n}\n\nfunction applyContextToSystem(output: SystemTransformOutput, context: string): void {\n output.system[0] = injectHindsightMemories(output.system[0] ?? \"\", context);\n}\n\nfunction capMapSize<K, V>(map: Map<K, V>, max: number): void {\n if (map.size <= max) return;\n const first = map.keys().next().value;\n if (first !== undefined) map.delete(first);\n}\n\nexport function createHooks(\n hindsightClient: HindsightClient,\n bankId: string,\n config: HindsightConfig,\n state: PluginState,\n opencodeClient: OpencodeClient,\n logger: Logger = new Logger({ silent: true })\n): HindsightHooks {\n interface RecallOutcome {\n context: string | null;\n ok: boolean;\n }\n\n async function recallForContext(query: string): Promise<RecallOutcome> {\n try {\n const response = await hindsightClient.recall(bankId, query, {\n budget: config.recallBudget as \"low\" | \"mid\" | \"high\",\n maxTokens: config.recallMaxTokens,\n types: config.recallTypes,\n tags: config.recallTags.length ? config.recallTags : undefined,\n tagsMatch: config.recallTags.length ? config.recallTagsMatch : undefined,\n });\n\n const results = response.results || [];\n if (!results.length) return { context: null, ok: true };\n\n const formatted = formatMemories(results);\n const context =\n `<hindsight_memories>\\n` +\n `${config.recallPromptPreamble}\\n` +\n `Current time: ${formatCurrentTime()} UTC\\n\\n` +\n `${formatted}\\n` +\n `</hindsight_memories>`;\n return { context, ok: true };\n } catch (e) {\n logger.error(\"Recall failed\", e);\n return { context: null, ok: false };\n }\n }\n\n async function getSessionMessages(sessionId: string): Promise<Message[]> {\n try {\n logger.debug(`getSessionMessages: fetching messages for session ${sessionId}`);\n const response = await opencodeClient.session.messages({\n path: { id: sessionId },\n });\n if (response.error) {\n logger.warn(\"getSessionMessages: OpenCode returned an error\", {\n error: JSON.stringify(response.error)?.substring(0, 500),\n });\n }\n const rawMessages = response.data || [];\n const messages: Message[] = [];\n for (const msg of rawMessages) {\n const role = msg.info.role;\n if (role !== \"user\" && role !== \"assistant\") continue;\n const textParts = msg.parts.filter((p) => p.type === \"text\" && p.text).map((p) => p.text!);\n if (textParts.length) {\n messages.push({ role, content: textParts.join(\"\\n\") });\n }\n }\n logger.debug(`getSessionMessages: raw=${rawMessages.length}, parsed=${messages.length}`);\n return messages;\n } catch (e) {\n logger.error(\"Failed to get session messages\", e);\n return [];\n }\n }\n\n async function retainSession(sessionId: string, messages: Message[]): Promise<void> {\n const retainFullWindow = config.retainMode === \"full-session\";\n let targetMessages: Message[];\n let documentId: string;\n\n if (retainFullWindow) {\n targetMessages = messages;\n documentId = sessionId;\n } else {\n const windowTurns = config.retainEveryNTurns + config.retainOverlapTurns;\n targetMessages = sliceLastTurnsByUserBoundary(messages, windowTurns);\n documentId = `${sessionId}-${Date.now()}`;\n }\n\n const { transcript } = prepareRetentionTranscript(targetMessages, true);\n if (!transcript) return;\n\n await ensureBankMission(hindsightClient, bankId, config, state.missionsSet, logger);\n await hindsightClient.retain(bankId, transcript, {\n documentId,\n context: config.retainContext,\n tags: config.retainTags.length ? config.retainTags : undefined,\n metadata: Object.keys(config.retainMetadata).length\n ? { ...config.retainMetadata, session_id: sessionId }\n : { session_id: sessionId },\n async: true,\n });\n }\n\n async function handleSessionIdle(sessionId: string): Promise<void> {\n logger.debug(`handleSessionIdle called for session ${sessionId}`);\n if (!config.autoRetain) return;\n\n const messages = await getSessionMessages(sessionId);\n if (!messages.length) return;\n\n const userTurns = countUserTurns(messages);\n const lastRetained = state.lastRetainedTurn.get(sessionId) || 0;\n logger.debug(\n `handleSessionIdle: userTurns=${userTurns}, lastRetained=${lastRetained}, retainEveryNTurns=${config.retainEveryNTurns}`\n );\n\n if (userTurns - lastRetained < config.retainEveryNTurns) return;\n\n try {\n await retainSession(sessionId, messages);\n state.lastRetainedTurn.set(sessionId, userTurns);\n logger.info(`Auto-retained ${messages.length} messages`, {\n session: sessionId,\n bank: bankId,\n });\n } catch (e) {\n logger.error(\"Auto-retain failed\", e);\n }\n }\n\n const event = async (input: EventInput): Promise<void> => {\n try {\n const { event: evt } = input;\n logger.debug(`event hook fired: type=${evt.type}`);\n\n if (evt.type === \"session.idle\") {\n const sessionId = (evt.properties as { sessionID?: string }).sessionID;\n if (sessionId) {\n await handleSessionIdle(sessionId);\n }\n }\n } catch (e) {\n logger.error(\"Event hook error\", e);\n }\n };\n\n const compacting = async (input: CompactingInput, output: CompactingOutput): Promise<void> => {\n try {\n const messages = await getSessionMessages(input.sessionID);\n if (messages.length && config.autoRetain) {\n try {\n await retainSession(input.sessionID, messages);\n state.lastRetainedTurn.delete(input.sessionID);\n logger.debug(\"Pre-compaction retain completed\");\n } catch (e) {\n logger.error(\"Pre-compaction retain failed\", e);\n }\n }\n\n if (messages.length) {\n const lastUserMsg = lastUserMessage(messages);\n if (lastUserMsg) {\n const query = composeRecallQuery(\n lastUserMsg.content,\n messages,\n config.recallContextTurns\n );\n const truncated = truncateRecallQuery(\n query,\n lastUserMsg.content,\n config.recallMaxQueryChars\n );\n const { context } = await recallForContext(truncated);\n if (context) {\n output.context.push(context);\n }\n }\n }\n } catch (e) {\n logger.error(\"Compaction hook error\", e);\n }\n };\n\n /**\n * Per-user-turn auto-recall (Claude Code UserPromptSubmit).\n *\n * OpenCode may re-run system.transform on every model call inside a tool\n * loop. We call Hindsight only when the user-turn count advances; within the\n * same turn we re-inject the cached block without another API round-trip.\n */\n const systemTransform = async (\n input: SystemTransformInput,\n output: SystemTransformOutput\n ): Promise<void> => {\n try {\n if (!config.autoRecall) return;\n const sessionId = input.sessionID;\n if (!sessionId) return;\n\n const messages = await getSessionMessages(sessionId);\n const lastUser = lastUserMessage(messages);\n if (!lastUser) {\n logger.debug(`systemTransform: no user message yet for ${sessionId}`);\n return;\n }\n\n const prompt = lastUser.content.trim();\n if (prompt.length < config.minRecallPromptChars) {\n logger.debug(\n `systemTransform: prompt too short (${prompt.length} < ${config.minRecallPromptChars}), skip`\n );\n return;\n }\n\n const userTurns = countUserTurns(messages);\n const cached = state.turnRecall.get(sessionId);\n\n // Same user turn → re-inject cache (tool loop / multi-step model calls).\n if (cached && cached.userTurnCount === userTurns) {\n if (cached.context) {\n applyContextToSystem(output, cached.context);\n logger.debug(`Re-injected cached recall for session ${sessionId} turn ${userTurns}`);\n }\n return;\n }\n\n await ensureBankMission(hindsightClient, bankId, config, state.missionsSet, logger);\n\n const query = composeRecallQuery(prompt, messages, config.recallContextTurns);\n const truncated = truncateRecallQuery(query, prompt, config.recallMaxQueryChars);\n logger.debug(\n `systemTransform: recalling for turn ${userTurns}, queryLen=${truncated.length}`\n );\n\n const { context, ok } = await recallForContext(truncated);\n\n // Only cache after a successful API round-trip so transient failures retry.\n if (ok) {\n state.turnRecall.set(sessionId, { userTurnCount: userTurns, context });\n capMapSize(state.turnRecall, 1000);\n }\n\n if (context) {\n applyContextToSystem(output, context);\n logger.debug(`Injected per-turn recall for session ${sessionId} turn ${userTurns}`);\n }\n } catch (e) {\n logger.error(\"System transform hook error\", e);\n }\n };\n\n return {\n event,\n \"experimental.session.compacting\": compacting,\n \"experimental.chat.system.transform\": systemTransform,\n };\n}\n"],"mappings":"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","names":["state"]}
package/package.json ADDED
@@ -0,0 +1,67 @@
1
+ {
2
+ "name": "opencode-hindsight-plus",
3
+ "version": "0.3.0",
4
+ "description": "Hindsight memory plugin for OpenCode with Claude Code-aligned per-turn auto-recall",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "import": "./dist/index.js"
12
+ }
13
+ },
14
+ "keywords": [
15
+ "opencode",
16
+ "ai",
17
+ "memory",
18
+ "hindsight",
19
+ "agents",
20
+ "llm",
21
+ "long-term-memory",
22
+ "coding-agent"
23
+ ],
24
+ "author": "best-linux-code",
25
+ "license": "MIT",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/best-linux-code/opencode-hindsight-plus.git"
29
+ },
30
+ "bugs": {
31
+ "url": "https://github.com/best-linux-code/opencode-hindsight-plus/issues"
32
+ },
33
+ "homepage": "https://github.com/best-linux-code/opencode-hindsight-plus#readme",
34
+ "files": [
35
+ "dist",
36
+ "README.md",
37
+ "LICENSE"
38
+ ],
39
+ "scripts": {
40
+ "build": "tsup",
41
+ "dev": "tsc --watch",
42
+ "clean": "rm -rf dist",
43
+ "test": "vitest run",
44
+ "test:watch": "vitest",
45
+ "test:e2e": "HINDSIGHT_LIVE_E2E=1 vitest run",
46
+ "prepublishOnly": "npm run clean && npm run build"
47
+ },
48
+ "dependencies": {
49
+ "@opencode-ai/plugin": "^1.3.13",
50
+ "@vectorize-io/hindsight-client": "^0.4.19"
51
+ },
52
+ "devDependencies": {
53
+ "@types/node": "^22.0.0",
54
+ "tsup": "^8.5.1",
55
+ "typescript": "^5.7.0",
56
+ "vitest": "^4.0.18"
57
+ },
58
+ "engines": {
59
+ "node": ">=22"
60
+ },
61
+ "overrides": {
62
+ "rollup": "^4.59.0",
63
+ "picomatch": ">=2.3.2 <3.0.0 || >=4.0.4",
64
+ "vite": ">=8.0.5",
65
+ "esbuild": "^0.28.1"
66
+ }
67
+ }