@ephemushroom/opencode-claude-mem 0.4.4 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,12 @@
1
+ export interface InjectionOptions {
2
+ readonly enabled: boolean;
3
+ readonly limit: number;
4
+ readonly maxChars: number;
5
+ readonly timeoutMs: number;
6
+ }
7
+ export interface MemoryOptions {
8
+ readonly semanticInjection: InjectionOptions;
9
+ readonly fileContext: InjectionOptions;
10
+ }
11
+ export declare function parseMemoryOptions(options: unknown, env: Readonly<Record<string, string | undefined>>, settings: unknown): MemoryOptions;
12
+ export declare function resolveMemoryOptions(options: unknown): MemoryOptions;
@@ -0,0 +1,22 @@
1
+ import type { MemoryOptions } from './memory-options';
2
+ import type { MemoryTurn } from './memory-turn';
3
+ import { MemoryWorker } from './memory-worker';
4
+ export declare class MemorySessions {
5
+ private readonly sessions;
6
+ private readonly deleted;
7
+ private readonly options;
8
+ private readonly worker;
9
+ constructor(options: MemoryOptions, worker?: typeof MemoryWorker);
10
+ private state;
11
+ select(session: string, turn: MemoryTurn): boolean;
12
+ hasTurn(session: string): boolean;
13
+ hasMessage(session: string, id: string): boolean;
14
+ isDeleted(session: string): boolean;
15
+ canCapture(session: string): boolean;
16
+ register(session: string, project: string): Promise<boolean>;
17
+ semantic(session: string, project: string): Promise<string>;
18
+ context(session: string, project: string): Promise<string | null>;
19
+ invalidate(session: string): void;
20
+ delete(session: string): void;
21
+ clear(): void;
22
+ }
@@ -0,0 +1,10 @@
1
+ export interface MemoryTurn {
2
+ readonly id: string;
3
+ readonly text: string;
4
+ readonly capture: boolean;
5
+ readonly semantic: boolean;
6
+ }
7
+ export declare function memoryTurn(id: string, text: string, media?: boolean): MemoryTurn;
8
+ /** Only consumed session context, never the admission/inbox list. */
9
+ export declare function deliveredTurn(messages: readonly unknown[]): MemoryTurn | undefined;
10
+ export declare function resumedV1Turn(messages: unknown): MemoryTurn | undefined;
@@ -0,0 +1,21 @@
1
+ import type { InjectionOptions } from './memory-options';
2
+ import { z } from 'zod';
3
+ declare const fileResponse: z.ZodObject<{
4
+ observations: z.ZodArray<z.ZodObject<{
5
+ id: z.ZodNumber;
6
+ title: z.ZodNullable<z.ZodString>;
7
+ type: z.ZodString;
8
+ created_at_epoch: z.ZodNumber;
9
+ memory_session_id: z.ZodOptional<z.ZodNullable<z.ZodString>>;
10
+ }, z.core.$strip>>;
11
+ count: z.ZodNumber;
12
+ }, z.core.$strip>;
13
+ export type FileObservation = z.infer<typeof fileResponse>['observations'][number];
14
+ export declare class MemoryWorker {
15
+ private static readonly baseUrl;
16
+ private static json;
17
+ static save(input: unknown, project: string): Promise<string>;
18
+ static semantic(prompt: string, project: string, options: InjectionOptions): Promise<string>;
19
+ static byFile(paths: readonly string[], project: string, options: InjectionOptions): Promise<readonly FileObservation[]>;
20
+ }
21
+ export {};
@@ -0,0 +1,7 @@
1
+ export { ClaudeMemPlugin } from './index';
2
+ declare const _default: {
3
+ id: string;
4
+ server: import("@opencode-ai/plugin").Plugin;
5
+ setup: (context: import("@opencode-ai/plugin-v2/promise/plugin").Context) => Promise<import("@opencode-ai/plugin-v2/promise/plugin").Cleanup | void> | import("@opencode-ai/plugin-v2/promise/plugin").Cleanup | void;
6
+ };
7
+ export default _default;
@@ -0,0 +1,10 @@
1
+ // @bun
2
+ // src/package-server.ts
3
+ import v1 from "./index";
4
+ import v2 from "./v2";
5
+ import { ClaudeMemPlugin } from "./index";
6
+ var package_server_default = { id: v2.id, server: v1.server, setup: v2.setup };
7
+ export {
8
+ ClaudeMemPlugin,
9
+ package_server_default as default
10
+ };
@@ -0,0 +1,6 @@
1
+ declare const _default: {
2
+ id: string;
3
+ tui: (api: any) => Promise<void>;
4
+ setup: (context: import("@opencode-ai/plugin-v2/tui/context").Context) => Promise<import("@opencode-ai/plugin-v2/tui/plugin").Cleanup | void> | import("@opencode-ai/plugin-v2/tui/plugin").Cleanup | void;
5
+ };
6
+ export default _default;
@@ -0,0 +1,8 @@
1
+ // @bun
2
+ // src/package-tui.ts
3
+ import v1 from "./tui.js";
4
+ import v2 from "./cli.js";
5
+ var package_tui_default = { id: v2.id, tui: v1.tui, setup: v2.setup };
6
+ export {
7
+ package_tui_default as default
8
+ };
@@ -0,0 +1,14 @@
1
+ export declare const MAX_OBSERVATION_BYTES: number;
2
+ export declare const MAX_TAG_REPLACEMENTS = 100;
3
+ export declare const META_TOOLS: Set<string>;
4
+ export declare function stripTaggedContent(text: string): string;
5
+ export declare function sanitizeObservationValue(value: unknown): unknown;
6
+ export declare function truncateUtf8Bytes(text: string, maxBytes: number): string;
7
+ export declare function shouldSkipObservationTool(toolName: string): boolean;
8
+ export declare function normalizeToolOutput(output: unknown): string;
9
+ /**
10
+ * Extract text content from message parts.
11
+ * Parts can be TextPart, ToolCallPart, etc. We only want text.
12
+ * Skips synthetic/ignored parts to match what is shown to the user.
13
+ */
14
+ export declare function extractTextFromParts(parts: any[]): string;
@@ -0,0 +1,50 @@
1
+ export declare const POLL_INTERVAL_MS = 5000;
2
+ export declare const OBSERVATION_TYPE_ICONS: Record<string, string>;
3
+ export interface MemSidebarView {
4
+ readonly healthy: boolean;
5
+ readonly project: string;
6
+ readonly observations: number | null;
7
+ readonly sessions: number | null;
8
+ readonly summaries: number | null;
9
+ readonly queueDepth: number | null;
10
+ readonly isProcessing: boolean;
11
+ readonly recentSummaries: readonly {
12
+ id: number;
13
+ request: string;
14
+ }[];
15
+ readonly recentObservations: readonly {
16
+ id: number;
17
+ type: string;
18
+ title: string;
19
+ }[];
20
+ }
21
+ export interface ViewNode {
22
+ readonly kind: 'box' | 'text';
23
+ readonly props: Record<string, unknown>;
24
+ readonly children?: readonly ViewNode[];
25
+ readonly text?: string;
26
+ }
27
+ export interface Theme {
28
+ readonly text: unknown;
29
+ readonly textMuted: unknown;
30
+ readonly info: unknown;
31
+ readonly success: unknown;
32
+ readonly warning: unknown;
33
+ readonly error: unknown;
34
+ readonly borderSubtle: unknown;
35
+ }
36
+ export declare function box(props: Record<string, unknown>, children: readonly ViewNode[]): ViewNode;
37
+ export declare function text(props: Record<string, unknown>, value: string): ViewNode;
38
+ export declare function decodeEntities(value: string): string;
39
+ export declare function truncate(value: string): string;
40
+ export declare function formatCount(value: number | null): string;
41
+ export declare function summaryLine(view: MemSidebarView): {
42
+ label: string;
43
+ tone: 'ok' | 'warn' | 'err';
44
+ };
45
+ export declare function toneColor(tone: 'ok' | 'warn' | 'err', theme: Theme): unknown;
46
+ export declare function bulletRow(icon: string, iconColor: unknown, label: string, theme: Theme): ViewNode;
47
+ export declare function expandedRows(view: MemSidebarView, theme: Theme): ViewNode[];
48
+ export declare function buildMemNodes(view: MemSidebarView, theme: Theme, collapsed: boolean, onToggle: () => void): ViewNode[];
49
+ export declare function viewKey(view: MemSidebarView): string;
50
+ export declare function readMemView(project: string, collapsed: boolean): Promise<MemSidebarView>;
@@ -7,3 +7,10 @@ export type TuiRegistrationResult = 'added' | 'already-present' | 'no-server-ent
7
7
  * target instead of being replaced by a regular file.
8
8
  */
9
9
  export declare function ensureTuiPluginEntry(): TuiRegistrationResult;
10
+ /**
11
+ * V2 self-heal: register the V2 CLI entry in `~/.config/opencode/cli.json`
12
+ * (the V2 CLI/TUI config file) so the Memory sidebar loads in the V2 TUI
13
+ * without manual configuration. The CLI writes strict JSON (2-space indent),
14
+ * so a comment-bearing file is left untouched ('malformed').
15
+ */
16
+ export declare function ensureCliPluginEntry(): TuiRegistrationResult;
package/dist/tui.d.ts CHANGED
@@ -1,39 +1,3 @@
1
- export interface MemSidebarView {
2
- readonly healthy: boolean;
3
- readonly project: string;
4
- readonly observations: number | null;
5
- readonly sessions: number | null;
6
- readonly summaries: number | null;
7
- readonly queueDepth: number | null;
8
- readonly isProcessing: boolean;
9
- readonly recentSummaries: readonly {
10
- id: number;
11
- request: string;
12
- }[];
13
- readonly recentObservations: readonly {
14
- id: number;
15
- type: string;
16
- title: string;
17
- }[];
18
- }
19
- interface ViewNode {
20
- readonly kind: 'box' | 'text';
21
- readonly props: Record<string, unknown>;
22
- readonly children?: readonly ViewNode[];
23
- readonly text?: string;
24
- }
25
- interface Theme {
26
- readonly text: string;
27
- readonly textMuted: string;
28
- readonly info: string;
29
- readonly success: string;
30
- readonly warning: string;
31
- readonly error: string;
32
- readonly borderSubtle: string;
33
- }
34
- export declare function buildMemNodes(view: MemSidebarView, theme: Theme, collapsed: boolean, onToggle: () => void): ViewNode[];
35
- export declare function viewKey(view: MemSidebarView): string;
36
- export declare function readMemView(project: string, collapsed: boolean): Promise<MemSidebarView>;
37
1
  declare const tuiModule: {
38
2
  id: string;
39
3
  tui: (api: any) => Promise<void>;
package/dist/tui.js CHANGED
@@ -1,12 +1,51 @@
1
1
  // @bun
2
- var __require = import.meta.require;
3
-
4
2
  // src/worker-client.ts
5
3
  import { existsSync, readFileSync } from "fs";
6
4
  import { homedir } from "os";
7
5
  import { join } from "path";
8
6
  import { spawn } from "child_process";
7
+
8
+ // src/shared.ts
9
+ var MAX_OBSERVATION_BYTES = 24 * 1024;
10
+ var META_TOOLS = new Set([
11
+ "askuserquestion",
12
+ "getmcpresource",
13
+ "listmcpresourcestool",
14
+ "listmcptools",
15
+ "mem-get-observations",
16
+ "mem-search",
17
+ "mem-timeline",
18
+ "skill",
19
+ "slashcommand",
20
+ "todowrite"
21
+ ]);
22
+ function stripTaggedContent(text) {
23
+ let result = "";
24
+ let offset = 0;
25
+ const stack = [];
26
+ for (const match of text.matchAll(/<(\/?)(private|claude-mem-context)\b[^>]*>/gi)) {
27
+ if (stack.length === 0) {
28
+ result += text.slice(offset, match.index);
29
+ }
30
+ const name = match[2].toLowerCase();
31
+ if (match[1] === "/") {
32
+ if (stack.at(-1) === name) {
33
+ stack.pop();
34
+ }
35
+ } else {
36
+ stack.push(name);
37
+ }
38
+ offset = match.index + match[0].length;
39
+ }
40
+ if (stack.length === 0) {
41
+ result += text.slice(offset);
42
+ }
43
+ return result.trim();
44
+ }
45
+
46
+ // src/worker-client.ts
9
47
  var HEALTH_TIMEOUT_MS = 1000;
48
+ var WRITE_TIMEOUT_MS = 1e4;
10
49
  var AUTOSTART_POLL_INTERVAL_MS = 500;
11
50
  var AUTOSTART_POLL_MAX_ATTEMPTS = 16;
12
51
  var DEFAULT_WORKER_HOST = "127.0.0.1";
@@ -92,14 +131,14 @@ function extractTextContent(value) {
92
131
  return content.trim() || null;
93
132
  }
94
133
  if (Array.isArray(content)) {
95
- const text2 = content.map((part) => {
134
+ const text = content.map((part) => {
96
135
  if (!isRecord(part) || part["type"] !== "text" || typeof part["text"] !== "string") {
97
136
  return "";
98
137
  }
99
138
  return part["text"];
100
139
  }).filter(Boolean).join(`
101
140
  `).trim();
102
- return text2 || null;
141
+ return text || null;
103
142
  }
104
143
  const text = JSON.stringify(value, null, 2);
105
144
  return text === "{}" || text === "null" ? null : text;
@@ -110,20 +149,17 @@ class WorkerClient {
110
149
  static autoStartPromise = null;
111
150
  static async isHealthy() {
112
151
  try {
113
- const controller = new AbortController;
114
- const timeoutId = setTimeout(() => controller.abort(), HEALTH_TIMEOUT_MS);
115
152
  const response = await fetch(`${this.BASE_URL}/api/health`, {
116
- signal: controller.signal
153
+ signal: AbortSignal.timeout(HEALTH_TIMEOUT_MS)
117
154
  });
118
- clearTimeout(timeoutId);
119
155
  return response.ok;
120
156
  } catch {
121
157
  return false;
122
158
  }
123
159
  }
124
160
  static async ensureRunning() {
125
- const result = await this.tryAutoStart();
126
- return result === "already-running" || result === "started";
161
+ await this.tryAutoStart();
162
+ return this.isHealthy();
127
163
  }
128
164
  static tryAutoStart() {
129
165
  if (!this.autoStartPromise) {
@@ -191,6 +227,7 @@ class WorkerClient {
191
227
  try {
192
228
  const response = await fetch(`${this.BASE_URL}/api/sessions/init`, {
193
229
  method: "POST",
230
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
194
231
  headers: { "Content-Type": "application/json" },
195
232
  body: JSON.stringify({
196
233
  contentSessionId,
@@ -202,15 +239,25 @@ class WorkerClient {
202
239
  if (!response.ok) {
203
240
  return null;
204
241
  }
205
- return await response.json();
242
+ const value = await response.json();
243
+ if (!isRecord(value) || typeof value.sessionDbId !== "number" || typeof value.promptNumber !== "number" || !Number.isFinite(value.sessionDbId) || !Number.isFinite(value.promptNumber)) {
244
+ return null;
245
+ }
246
+ return {
247
+ sessionDbId: value.sessionDbId,
248
+ promptNumber: value.promptNumber,
249
+ skipped: value.skipped === true,
250
+ reason: typeof value.reason === "string" ? value.reason : undefined
251
+ };
206
252
  } catch {
207
253
  return null;
208
254
  }
209
255
  }
210
- static async sendObservation(contentSessionId, toolName, toolInput, toolResponse, cwd) {
256
+ static async sendObservation(contentSessionId, toolName, toolInput, toolResponse, cwd, toolUseId) {
211
257
  try {
212
- await fetch(`${this.BASE_URL}/api/sessions/observations`, {
258
+ const response = await fetch(`${this.BASE_URL}/api/sessions/observations`, {
213
259
  method: "POST",
260
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
214
261
  headers: { "Content-Type": "application/json" },
215
262
  body: JSON.stringify({
216
263
  contentSessionId,
@@ -218,37 +265,38 @@ class WorkerClient {
218
265
  tool_input: toolInput,
219
266
  tool_response: toolResponse,
220
267
  cwd,
221
- platformSource: PLATFORM_SOURCE
268
+ platformSource: PLATFORM_SOURCE,
269
+ tool_use_id: toolUseId
222
270
  })
223
271
  });
224
- } catch {}
272
+ return response.ok;
273
+ } catch {
274
+ return false;
275
+ }
225
276
  }
226
- static async summarize(contentSessionId, lastUserMessage, lastAssistantMessage) {
277
+ static async summarize(contentSessionId, lastUserMessage, lastAssistantMessage, metadata = {}) {
227
278
  try {
228
- await fetch(`${this.BASE_URL}/api/sessions/summarize`, {
279
+ const response = await fetch(`${this.BASE_URL}/api/sessions/summarize`, {
229
280
  method: "POST",
281
+ signal: AbortSignal.timeout(WRITE_TIMEOUT_MS),
230
282
  headers: { "Content-Type": "application/json" },
231
283
  body: JSON.stringify({
232
284
  contentSessionId,
233
- last_user_message: lastUserMessage,
234
- last_assistant_message: lastAssistantMessage,
235
- platformSource: PLATFORM_SOURCE
285
+ last_user_message: stripTaggedContent(lastUserMessage),
286
+ last_assistant_message: stripTaggedContent(lastAssistantMessage),
287
+ platformSource: PLATFORM_SOURCE,
288
+ observedModel: metadata.observedModel,
289
+ observedBilling: metadata.observedBilling
236
290
  })
237
291
  });
238
- } catch {}
239
- }
240
- static async completeSession(contentSessionId) {
241
- try {
242
- await fetch(`${this.BASE_URL}/api/sessions/complete`, {
243
- method: "POST",
244
- headers: { "Content-Type": "application/json" },
245
- body: JSON.stringify({ contentSessionId, platformSource: PLATFORM_SOURCE })
246
- });
247
- } catch {}
292
+ return response.ok;
293
+ } catch {
294
+ return false;
295
+ }
248
296
  }
249
297
  static async getContext(project) {
250
298
  try {
251
- const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}`);
299
+ const response = await fetch(`${this.BASE_URL}/api/context/inject?project=${encodeURIComponent(project)}`, { signal: AbortSignal.timeout(2000) });
252
300
  if (!response.ok) {
253
301
  return null;
254
302
  }
@@ -399,7 +447,7 @@ class WorkerClient {
399
447
  }
400
448
  }
401
449
 
402
- // src/tui.ts
450
+ // src/sidebar-model.ts
403
451
  var POLL_INTERVAL_MS = 5000;
404
452
  var LABEL_MAX = 40;
405
453
  var RECENT_SUMMARIES = 3;
@@ -537,6 +585,8 @@ async function readMemView(project, collapsed) {
537
585
  recentObservations
538
586
  };
539
587
  }
588
+
589
+ // src/tui.ts
540
590
  async function loadSolidCore() {
541
591
  try {
542
592
  const mod = await import("solid-js");
@@ -652,8 +702,5 @@ var tuiModule = {
652
702
  };
653
703
  var tui_default = tuiModule;
654
704
  export {
655
- viewKey,
656
- readMemView,
657
- tui_default as default,
658
- buildMemNodes
705
+ tui_default as default
659
706
  };
package/dist/v2.d.ts ADDED
@@ -0,0 +1,22 @@
1
+ /**
2
+ * OpenCode V2 plugin for Claude-Mem.
3
+ *
4
+ * V2 is a breaking API change from V1 — plugins are `Plugin.define({ id,
5
+ * setup })` modules with a server-like context instead of a `{ server }`
6
+ * module of hook handlers. This entrypoint is published under the package's
7
+ * `./v2` export; V1 users keep loading the default export (see src/index.ts).
8
+ * Both entrypoints are thin adapters over the same WorkerClient + shared
9
+ * helpers, so V1 and V2 can coexist and talk to the same Claude-Mem worker.
10
+ *
11
+ * V2 API mapping (vs V1):
12
+ * - `experimental.chat.system.transform` → `ctx.session.hook('context')` (push
13
+ * a SystemPart into `event.system`)
14
+ * - `tool.execute.after` → `ctx.tool.hook('execute.after')`
15
+ * - `tool` definitions → `ctx.tool.transform((tools) => tools.add(...))`
16
+ * - `event` lifecycle → `ctx.event.subscribe()` stream
17
+ * - Assistant text capture uses `session.text.ended` events (the event carries
18
+ * the complete text — no debounced re-fetch needed like V1's message.updated)
19
+ * - Summarization fires on `session.execution.succeeded` (V1's session.idle)
20
+ */
21
+ declare const _default: import("@opencode-ai/plugin-v2/promise/plugin").Plugin;
22
+ export default _default;