opencode-mem 2.11.12 → 2.12.1

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/README.md CHANGED
@@ -74,11 +74,9 @@ Configure at `~/.config/opencode/opencode-mem.jsonc`:
74
74
 
75
75
  "autoCaptureEnabled": true,
76
76
  "autoCaptureLanguage": "auto",
77
- "memoryProvider": "openai-chat",
78
- "memoryModel": "gpt-4o-mini",
79
- "memoryApiUrl": "https://api.openai.com/v1",
80
- "memoryApiKey": "sk-...",
81
- "memoryTemperature": 0.3,
77
+
78
+ "opencodeProvider": "anthropic",
79
+ "opencodeModel": "claude-haiku-4-5-20251001",
82
80
 
83
81
  "showAutoCaptureToasts": true,
84
82
  "showUserProfileToasts": true,
@@ -101,6 +99,28 @@ Configure at `~/.config/opencode/opencode-mem.jsonc`:
101
99
  }
102
100
  ```
103
101
 
102
+ ### Auto-Capture AI Provider
103
+
104
+ **Recommended:** Use opencode's built-in providers (no separate API key needed):
105
+
106
+ ```jsonc
107
+ "opencodeProvider": "anthropic",
108
+ "opencodeModel": "claude-haiku-4-5-20251001",
109
+ ```
110
+
111
+ This leverages your existing opencode authentication (OAuth or API key). Works with Claude Pro/Max plans via OAuth - no individual API keys required.
112
+
113
+ Supported providers: `anthropic`, `openai`
114
+
115
+ **Fallback:** Manual API configuration (if not using opencodeProvider):
116
+
117
+ ```jsonc
118
+ "memoryProvider": "openai-chat",
119
+ "memoryModel": "gpt-4o-mini",
120
+ "memoryApiUrl": "https://api.openai.com/v1",
121
+ "memoryApiKey": "sk-...",
122
+ ```
123
+
104
124
  **API Key Formats:**
105
125
 
106
126
  ```jsonc
package/dist/config.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- export declare const CONFIG: {
1
+ export declare let CONFIG: {
2
2
  storagePath: string;
3
3
  userEmailOverride: string | undefined;
4
4
  userNameOverride: string | undefined;
@@ -21,6 +21,8 @@ export declare const CONFIG: {
21
21
  memoryApiKey: string | undefined;
22
22
  memoryTemperature: number | false | undefined;
23
23
  memoryExtraParams: Record<string, unknown> | undefined;
24
+ opencodeProvider: string | undefined;
25
+ opencodeModel: string | undefined;
24
26
  vectorBackend: "usearch-first" | "usearch" | "exact-scan";
25
27
  aiSessionRetentionDays: number;
26
28
  webServerEnabled: boolean;
@@ -52,5 +54,6 @@ export declare const CONFIG: {
52
54
  injectOn: "first" | "always";
53
55
  };
54
56
  };
57
+ export declare function initConfig(directory: string): void;
55
58
  export declare function isConfigured(): boolean;
56
59
  //# sourceMappingURL=config.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AA6aA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;oBAwBb,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;;mBAOX,eAAe,GACf,SAAS,GACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAoCV,OAAO,GACP,QAAQ;;CAEf,CAAC;AAEF,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAohBA,eAAO,IAAI,MAAM;;;;;;;;;;;;;;;;;oBAxDT,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;;;;mBASX,eAAe,GACf,SAAS,GACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAoCV,OAAO,GACP,QAAQ;;CAMgC,CAAC;AAEnD,wBAAgB,UAAU,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CASlD;AAED,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
package/dist/config.js CHANGED
@@ -67,8 +67,8 @@ function expandPath(path) {
67
67
  }
68
68
  return path;
69
69
  }
70
- function loadConfig() {
71
- for (const path of CONFIG_FILES) {
70
+ function loadConfigFromPaths(paths) {
71
+ for (const path of paths) {
72
72
  if (existsSync(path)) {
73
73
  try {
74
74
  const content = readFileSync(path, "utf-8");
@@ -80,7 +80,6 @@ function loadConfig() {
80
80
  }
81
81
  return {};
82
82
  }
83
- const fileConfig = loadConfig();
84
83
  const CONFIG_TEMPLATE = `{
85
84
  // ============================================
86
85
  // OpenCode Memory Plugin Configuration
@@ -142,12 +141,30 @@ const CONFIG_TEMPLATE = `{
142
141
  // Automatically detect and remove duplicate memories
143
142
  "deduplicationEnabled": true,
144
143
 
145
- // Similarity threshold (0-1) for detecting duplicates (higher = stricter)
146
- "deduplicationSimilarityThreshold": 0.90,
147
-
148
- // ============================================
149
- // Auto-Capture Settings (REQUIRES EXTERNAL API)
150
- // ============================================
144
+ // Similarity threshold (0-1) for detecting duplicates (higher = stricter)
145
+ "deduplicationSimilarityThreshold": 0.90,
146
+
147
+ // ============================================
148
+ // OpenCode Provider Settings (RECOMMENDED)
149
+ // ============================================
150
+
151
+ // Use opencode's already-configured providers for auto-capture and user profile learning.
152
+ // When set, no separate API key is needed — uses your existing opencode authentication
153
+ // (including Claude Pro/Max plans via OAuth, or any API key configured in opencode).
154
+ //
155
+ // If NOT set, falls back to the manual config (memoryApiKey/memoryApiUrl/memoryModel below).
156
+ //
157
+ // Examples:
158
+ // Anthropic (OAuth/API key): "opencodeProvider": "anthropic", "opencodeModel": "claude-haiku-4-5-20251001"
159
+ // OpenAI (API key): "opencodeProvider": "openai", "opencodeModel": "gpt-4o-mini"
160
+ //
161
+ // The provider name must match a connected provider in opencode (check with: opencode providers list)
162
+ // "opencodeProvider": "anthropic",
163
+ // "opencodeModel": "claude-haiku-4-5-20251001",
164
+
165
+ // ============================================
166
+ // Auto-Capture Settings (REQUIRES EXTERNAL API)
167
+ // ============================================
151
168
 
152
169
  // IMPORTANT: Auto-capture ONLY works with external API
153
170
  // It runs in background without blocking your main session
@@ -329,63 +346,79 @@ function getEmbeddingDimensions(model) {
329
346
  };
330
347
  return dimensionMap[model] || 768;
331
348
  }
332
- export const CONFIG = {
333
- storagePath: expandPath(fileConfig.storagePath ?? DEFAULTS.storagePath),
334
- userEmailOverride: fileConfig.userEmailOverride,
335
- userNameOverride: fileConfig.userNameOverride,
336
- embeddingModel: fileConfig.embeddingModel ?? DEFAULTS.embeddingModel,
337
- embeddingDimensions: fileConfig.embeddingDimensions ??
338
- getEmbeddingDimensions(fileConfig.embeddingModel ?? DEFAULTS.embeddingModel),
339
- embeddingApiUrl: fileConfig.embeddingApiUrl,
340
- embeddingApiKey: fileConfig.embeddingApiUrl
341
- ? resolveSecretValue(fileConfig.embeddingApiKey ?? process.env.OPENAI_API_KEY)
342
- : undefined,
343
- similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold,
344
- maxMemories: fileConfig.maxMemories ?? DEFAULTS.maxMemories,
345
- maxProfileItems: fileConfig.maxProfileItems ?? DEFAULTS.maxProfileItems,
346
- injectProfile: fileConfig.injectProfile ?? DEFAULTS.injectProfile,
347
- containerTagPrefix: fileConfig.containerTagPrefix ?? DEFAULTS.containerTagPrefix,
348
- autoCaptureEnabled: fileConfig.autoCaptureEnabled ?? DEFAULTS.autoCaptureEnabled,
349
- autoCaptureMaxIterations: fileConfig.autoCaptureMaxIterations ?? DEFAULTS.autoCaptureMaxIterations,
350
- autoCaptureIterationTimeout: fileConfig.autoCaptureIterationTimeout ?? DEFAULTS.autoCaptureIterationTimeout,
351
- autoCaptureLanguage: fileConfig.autoCaptureLanguage,
352
- memoryProvider: (fileConfig.memoryProvider ?? "openai-chat"),
353
- memoryModel: fileConfig.memoryModel,
354
- memoryApiUrl: fileConfig.memoryApiUrl,
355
- memoryApiKey: resolveSecretValue(fileConfig.memoryApiKey),
356
- memoryTemperature: fileConfig.memoryTemperature,
357
- memoryExtraParams: fileConfig.memoryExtraParams,
358
- vectorBackend: (fileConfig.vectorBackend ?? "usearch-first"),
359
- aiSessionRetentionDays: fileConfig.aiSessionRetentionDays ?? DEFAULTS.aiSessionRetentionDays,
360
- webServerEnabled: fileConfig.webServerEnabled ?? DEFAULTS.webServerEnabled,
361
- webServerPort: fileConfig.webServerPort ?? DEFAULTS.webServerPort,
362
- webServerHost: fileConfig.webServerHost ?? DEFAULTS.webServerHost,
363
- maxVectorsPerShard: fileConfig.maxVectorsPerShard ?? DEFAULTS.maxVectorsPerShard,
364
- autoCleanupEnabled: fileConfig.autoCleanupEnabled ?? DEFAULTS.autoCleanupEnabled,
365
- autoCleanupRetentionDays: fileConfig.autoCleanupRetentionDays ?? DEFAULTS.autoCleanupRetentionDays,
366
- deduplicationEnabled: fileConfig.deduplicationEnabled ?? DEFAULTS.deduplicationEnabled,
367
- deduplicationSimilarityThreshold: fileConfig.deduplicationSimilarityThreshold ?? DEFAULTS.deduplicationSimilarityThreshold,
368
- userProfileAnalysisInterval: fileConfig.userProfileAnalysisInterval ?? DEFAULTS.userProfileAnalysisInterval,
369
- userProfileMaxPreferences: fileConfig.userProfileMaxPreferences ?? DEFAULTS.userProfileMaxPreferences,
370
- userProfileMaxPatterns: fileConfig.userProfileMaxPatterns ?? DEFAULTS.userProfileMaxPatterns,
371
- userProfileMaxWorkflows: fileConfig.userProfileMaxWorkflows ?? DEFAULTS.userProfileMaxWorkflows,
372
- userProfileConfidenceDecayDays: fileConfig.userProfileConfidenceDecayDays ?? DEFAULTS.userProfileConfidenceDecayDays,
373
- userProfileChangelogRetentionCount: fileConfig.userProfileChangelogRetentionCount ?? DEFAULTS.userProfileChangelogRetentionCount,
374
- showAutoCaptureToasts: fileConfig.showAutoCaptureToasts ?? DEFAULTS.showAutoCaptureToasts,
375
- showUserProfileToasts: fileConfig.showUserProfileToasts ?? DEFAULTS.showUserProfileToasts,
376
- showErrorToasts: fileConfig.showErrorToasts ?? DEFAULTS.showErrorToasts,
377
- compaction: {
378
- enabled: fileConfig.compaction?.enabled ?? DEFAULTS.compaction.enabled,
379
- memoryLimit: fileConfig.compaction?.memoryLimit ?? DEFAULTS.compaction.memoryLimit,
380
- },
381
- chatMessage: {
382
- enabled: fileConfig.chatMessage?.enabled ?? DEFAULTS.chatMessage.enabled,
383
- maxMemories: fileConfig.chatMessage?.maxMemories ?? DEFAULTS.chatMessage.maxMemories,
384
- excludeCurrentSession: fileConfig.chatMessage?.excludeCurrentSession ?? DEFAULTS.chatMessage.excludeCurrentSession,
385
- maxAgeDays: fileConfig.chatMessage?.maxAgeDays,
386
- injectOn: (fileConfig.chatMessage?.injectOn ?? DEFAULTS.chatMessage.injectOn),
387
- },
388
- };
349
+ function buildConfig(fileConfig) {
350
+ return {
351
+ storagePath: expandPath(fileConfig.storagePath ?? DEFAULTS.storagePath),
352
+ userEmailOverride: fileConfig.userEmailOverride,
353
+ userNameOverride: fileConfig.userNameOverride,
354
+ embeddingModel: fileConfig.embeddingModel ?? DEFAULTS.embeddingModel,
355
+ embeddingDimensions: fileConfig.embeddingDimensions ??
356
+ getEmbeddingDimensions(fileConfig.embeddingModel ?? DEFAULTS.embeddingModel),
357
+ embeddingApiUrl: fileConfig.embeddingApiUrl,
358
+ embeddingApiKey: fileConfig.embeddingApiUrl
359
+ ? resolveSecretValue(fileConfig.embeddingApiKey ?? process.env.OPENAI_API_KEY)
360
+ : undefined,
361
+ similarityThreshold: fileConfig.similarityThreshold ?? DEFAULTS.similarityThreshold,
362
+ maxMemories: fileConfig.maxMemories ?? DEFAULTS.maxMemories,
363
+ maxProfileItems: fileConfig.maxProfileItems ?? DEFAULTS.maxProfileItems,
364
+ injectProfile: fileConfig.injectProfile ?? DEFAULTS.injectProfile,
365
+ containerTagPrefix: fileConfig.containerTagPrefix ?? DEFAULTS.containerTagPrefix,
366
+ autoCaptureEnabled: fileConfig.autoCaptureEnabled ?? DEFAULTS.autoCaptureEnabled,
367
+ autoCaptureMaxIterations: fileConfig.autoCaptureMaxIterations ?? DEFAULTS.autoCaptureMaxIterations,
368
+ autoCaptureIterationTimeout: fileConfig.autoCaptureIterationTimeout ?? DEFAULTS.autoCaptureIterationTimeout,
369
+ autoCaptureLanguage: fileConfig.autoCaptureLanguage,
370
+ memoryProvider: (fileConfig.memoryProvider ?? "openai-chat"),
371
+ memoryModel: fileConfig.memoryModel,
372
+ memoryApiUrl: fileConfig.memoryApiUrl,
373
+ memoryApiKey: resolveSecretValue(fileConfig.memoryApiKey),
374
+ memoryTemperature: fileConfig.memoryTemperature,
375
+ memoryExtraParams: fileConfig.memoryExtraParams,
376
+ opencodeProvider: fileConfig.opencodeProvider,
377
+ opencodeModel: fileConfig.opencodeModel,
378
+ vectorBackend: (fileConfig.vectorBackend ?? "usearch-first"),
379
+ aiSessionRetentionDays: fileConfig.aiSessionRetentionDays ?? DEFAULTS.aiSessionRetentionDays,
380
+ webServerEnabled: fileConfig.webServerEnabled ?? DEFAULTS.webServerEnabled,
381
+ webServerPort: fileConfig.webServerPort ?? DEFAULTS.webServerPort,
382
+ webServerHost: fileConfig.webServerHost ?? DEFAULTS.webServerHost,
383
+ maxVectorsPerShard: fileConfig.maxVectorsPerShard ?? DEFAULTS.maxVectorsPerShard,
384
+ autoCleanupEnabled: fileConfig.autoCleanupEnabled ?? DEFAULTS.autoCleanupEnabled,
385
+ autoCleanupRetentionDays: fileConfig.autoCleanupRetentionDays ?? DEFAULTS.autoCleanupRetentionDays,
386
+ deduplicationEnabled: fileConfig.deduplicationEnabled ?? DEFAULTS.deduplicationEnabled,
387
+ deduplicationSimilarityThreshold: fileConfig.deduplicationSimilarityThreshold ?? DEFAULTS.deduplicationSimilarityThreshold,
388
+ userProfileAnalysisInterval: fileConfig.userProfileAnalysisInterval ?? DEFAULTS.userProfileAnalysisInterval,
389
+ userProfileMaxPreferences: fileConfig.userProfileMaxPreferences ?? DEFAULTS.userProfileMaxPreferences,
390
+ userProfileMaxPatterns: fileConfig.userProfileMaxPatterns ?? DEFAULTS.userProfileMaxPatterns,
391
+ userProfileMaxWorkflows: fileConfig.userProfileMaxWorkflows ?? DEFAULTS.userProfileMaxWorkflows,
392
+ userProfileConfidenceDecayDays: fileConfig.userProfileConfidenceDecayDays ?? DEFAULTS.userProfileConfidenceDecayDays,
393
+ userProfileChangelogRetentionCount: fileConfig.userProfileChangelogRetentionCount ?? DEFAULTS.userProfileChangelogRetentionCount,
394
+ showAutoCaptureToasts: fileConfig.showAutoCaptureToasts ?? DEFAULTS.showAutoCaptureToasts,
395
+ showUserProfileToasts: fileConfig.showUserProfileToasts ?? DEFAULTS.showUserProfileToasts,
396
+ showErrorToasts: fileConfig.showErrorToasts ?? DEFAULTS.showErrorToasts,
397
+ compaction: {
398
+ enabled: fileConfig.compaction?.enabled ?? DEFAULTS.compaction.enabled,
399
+ memoryLimit: fileConfig.compaction?.memoryLimit ?? DEFAULTS.compaction.memoryLimit,
400
+ },
401
+ chatMessage: {
402
+ enabled: fileConfig.chatMessage?.enabled ?? DEFAULTS.chatMessage.enabled,
403
+ maxMemories: fileConfig.chatMessage?.maxMemories ?? DEFAULTS.chatMessage.maxMemories,
404
+ excludeCurrentSession: fileConfig.chatMessage?.excludeCurrentSession ?? DEFAULTS.chatMessage.excludeCurrentSession,
405
+ maxAgeDays: fileConfig.chatMessage?.maxAgeDays,
406
+ injectOn: (fileConfig.chatMessage?.injectOn ?? DEFAULTS.chatMessage.injectOn),
407
+ },
408
+ };
409
+ }
410
+ let _globalFileConfig = loadConfigFromPaths(CONFIG_FILES);
411
+ export let CONFIG = buildConfig(_globalFileConfig);
412
+ export function initConfig(directory) {
413
+ const projectPaths = [
414
+ join(directory, ".opencode", "opencode-mem.jsonc"),
415
+ join(directory, ".opencode", "opencode-mem.json"),
416
+ ];
417
+ const globalConfig = loadConfigFromPaths(CONFIG_FILES);
418
+ const projectConfig = loadConfigFromPaths(projectPaths);
419
+ const merged = { ...globalConfig, ...projectConfig };
420
+ CONFIG = buildConfig(merged);
421
+ }
389
422
  export function isConfigured() {
390
423
  return true;
391
424
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAkB/D,eAAO,MAAM,iBAAiB,EAAE,MA8a/B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,MAAM,EAAe,MAAM,qBAAqB,CAAC;AAmB/D,eAAO,MAAM,iBAAiB,EAAE,MAgc/B,CAAC"}
package/dist/index.js CHANGED
@@ -7,11 +7,13 @@ import { performAutoCapture } from "./services/auto-capture.js";
7
7
  import { performUserProfileLearning } from "./services/user-memory-learning.js";
8
8
  import { userPromptManager } from "./services/user-prompt/user-prompt-manager.js";
9
9
  import { startWebServer, WebServer } from "./services/web-server.js";
10
- import { isConfigured, CONFIG } from "./config.js";
10
+ import { isConfigured, CONFIG, initConfig } from "./config.js";
11
11
  import { log } from "./services/logger.js";
12
12
  import { getLanguageName } from "./services/language-detector.js";
13
+ import { setStatePath, setConnectedProviders } from "./services/ai/opencode-provider.js";
13
14
  export const OpenCodeMemPlugin = async (ctx) => {
14
15
  const { directory } = ctx;
16
+ initConfig(directory);
15
17
  const tags = getTags(directory);
16
18
  let webServer = null;
17
19
  let idleTimeout = null;
@@ -27,6 +29,23 @@ export const OpenCodeMemPlugin = async (ctx) => {
27
29
  log("Plugin warmup failed", { error: String(error) });
28
30
  }
29
31
  }
32
+ // Wire opencode state path and provider list — fire-and-forget to avoid blocking init
33
+ // These calls can hang if opencode isn't fully bootstrapped yet
34
+ (async () => {
35
+ try {
36
+ const pathResult = await ctx.client.path.get();
37
+ if (pathResult.data?.state) {
38
+ setStatePath(pathResult.data.state);
39
+ }
40
+ const providerResult = await ctx.client.provider.list();
41
+ if (providerResult.data?.connected) {
42
+ setConnectedProviders(providerResult.data.connected);
43
+ }
44
+ }
45
+ catch (error) {
46
+ log("Failed to initialize opencode provider state", { error: String(error) });
47
+ }
48
+ })();
30
49
  if (CONFIG.webServerEnabled) {
31
50
  startWebServer({
32
51
  port: CONFIG.webServerPort,
@@ -312,7 +331,7 @@ export const OpenCodeMemPlugin = async (ctx) => {
312
331
  event: async (input) => {
313
332
  const event = input.event;
314
333
  if (event.type === "session.idle") {
315
- if (!isConfigured())
334
+ if (!isConfigured() || !CONFIG.autoCaptureEnabled)
316
335
  return;
317
336
  const sessionID = event.properties?.sessionID;
318
337
  if (!sessionID)
@@ -0,0 +1,30 @@
1
+ import type { ZodType } from "zod";
2
+ type OAuthAuth = {
3
+ type: "oauth";
4
+ refresh: string;
5
+ access: string;
6
+ expires: number;
7
+ };
8
+ type ApiAuth = {
9
+ type: "api";
10
+ key: string;
11
+ };
12
+ type Auth = OAuthAuth | ApiAuth;
13
+ export declare function setStatePath(path: string): void;
14
+ export declare function getStatePath(): string;
15
+ export declare function setConnectedProviders(providers: string[]): void;
16
+ export declare function isProviderConnected(providerName: string): boolean;
17
+ export declare function readOpencodeAuth(statePath: string, providerName: string): Auth;
18
+ export declare function createOAuthFetch(statePath: string, providerName: string): (input: string | Request | URL, init?: RequestInit) => Promise<Response>;
19
+ export declare function createOpencodeAIProvider(providerName: string, auth: Auth, statePath?: string): import("@ai-sdk/anthropic").AnthropicProvider | import("@ai-sdk/openai").OpenAIProvider;
20
+ export declare function generateStructuredOutput<T>(options: {
21
+ providerName: string;
22
+ modelId: string;
23
+ statePath: string;
24
+ systemPrompt: string;
25
+ userPrompt: string;
26
+ schema: ZodType<T>;
27
+ temperature?: number;
28
+ }): Promise<T>;
29
+ export {};
30
+ //# sourceMappingURL=opencode-provider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"opencode-provider.d.ts","sourceRoot":"","sources":["../../../src/services/ai/opencode-provider.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,KAAK,CAAC;AAEnC,KAAK,SAAS,GAAG;IAAE,IAAI,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC;AACrF,KAAK,OAAO,GAAG;IAAE,IAAI,EAAE,KAAK,CAAC;IAAC,GAAG,EAAE,MAAM,CAAA;CAAE,CAAC;AAC5C,KAAK,IAAI,GAAG,SAAS,GAAG,OAAO,CAAC;AAMhC,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAE/C;AAED,wBAAgB,YAAY,IAAI,MAAM,CAKrC;AAED,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAE/D;AAED,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAEjE;AAYD,wBAAgB,gBAAgB,CAAC,SAAS,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,GAAG,IAAI,CA2B9E;AAQD,wBAAgB,gBAAgB,CAC9B,SAAS,EAAE,MAAM,EACjB,YAAY,EAAE,MAAM,GACnB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,GAAG,EAAE,IAAI,CAAC,EAAE,WAAW,KAAK,OAAO,CAAC,QAAQ,CAAC,CAkJ1E;AAGD,wBAAgB,wBAAwB,CAAC,YAAY,EAAE,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,SAAS,CAAC,EAAE,MAAM,2FAoB5F;AAGD,wBAAsB,wBAAwB,CAAC,CAAC,EAAE,OAAO,EAAE;IACzD,YAAY,EAAE,MAAM,CAAC;IACrB,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,GAAG,OAAO,CAAC,CAAC,CAAC,CAWb"}
@@ -0,0 +1,238 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { join, dirname } from "node:path";
3
+ import { generateText, Output } from "ai";
4
+ import { createAnthropic } from "@ai-sdk/anthropic";
5
+ import { createOpenAI } from "@ai-sdk/openai";
6
+ // --- State (set from plugin init in index.ts, Task 4) ---
7
+ let _statePath = null;
8
+ let _connectedProviders = [];
9
+ export function setStatePath(path) {
10
+ _statePath = path;
11
+ }
12
+ export function getStatePath() {
13
+ if (!_statePath) {
14
+ throw new Error("opencode state path not initialized. Plugin may not be fully started.");
15
+ }
16
+ return _statePath;
17
+ }
18
+ export function setConnectedProviders(providers) {
19
+ _connectedProviders = providers;
20
+ }
21
+ export function isProviderConnected(providerName) {
22
+ return _connectedProviders.includes(providerName);
23
+ }
24
+ // --- Auth ---
25
+ function findAuthJsonPath(statePath) {
26
+ const candidates = [
27
+ join(statePath, "auth.json"),
28
+ join(dirname(statePath), "share", "opencode", "auth.json"),
29
+ join(statePath.replace("/state/", "/share/"), "auth.json"),
30
+ ];
31
+ return candidates.find(existsSync);
32
+ }
33
+ export function readOpencodeAuth(statePath, providerName) {
34
+ const authPath = findAuthJsonPath(statePath);
35
+ let raw;
36
+ if (authPath) {
37
+ try {
38
+ raw = readFileSync(authPath, "utf-8");
39
+ }
40
+ catch { }
41
+ }
42
+ if (!raw || !authPath) {
43
+ throw new Error(`opencode auth.json not found at ${authPath ?? statePath}. Is opencode authenticated?`);
44
+ }
45
+ let parsed;
46
+ try {
47
+ parsed = JSON.parse(raw);
48
+ }
49
+ catch {
50
+ throw new Error(`Failed to read opencode auth.json: invalid JSON`);
51
+ }
52
+ const auth = parsed[providerName];
53
+ if (!auth) {
54
+ const connected = Object.keys(parsed).join(", ") || "none";
55
+ throw new Error(`Provider '${providerName}' not found in opencode auth.json. Connected providers: ${connected}`);
56
+ }
57
+ return auth;
58
+ }
59
+ // --- OAuth Fetch ---
60
+ const OAUTH_CLIENT_ID = "9d1c250a-e61b-44d9-88ed-5944d1962f5e";
61
+ const OAUTH_TOKEN_URL = "https://console.anthropic.com/v1/oauth/token";
62
+ const OAUTH_REQUIRED_BETAS = ["oauth-2025-04-20", "interleaved-thinking-2025-05-14"];
63
+ const MCP_TOOL_PREFIX = "mcp_";
64
+ export function createOAuthFetch(statePath, providerName) {
65
+ return async (input, init) => {
66
+ let auth = readOpencodeAuth(statePath, providerName);
67
+ // Refresh token if expired
68
+ if (!auth.access || auth.expires < Date.now()) {
69
+ const refreshResponse = await fetch(OAUTH_TOKEN_URL, {
70
+ method: "POST",
71
+ headers: { "Content-Type": "application/json" },
72
+ body: JSON.stringify({
73
+ grant_type: "refresh_token",
74
+ refresh_token: auth.refresh,
75
+ client_id: OAUTH_CLIENT_ID,
76
+ }),
77
+ });
78
+ if (!refreshResponse.ok) {
79
+ throw new Error(`OAuth token refresh failed: ${refreshResponse.status}`);
80
+ }
81
+ const json = (await refreshResponse.json());
82
+ auth = {
83
+ type: "oauth",
84
+ refresh: json.refresh_token,
85
+ access: json.access_token,
86
+ expires: Date.now() + json.expires_in * 1000,
87
+ };
88
+ const authPath = findAuthJsonPath(statePath);
89
+ if (authPath) {
90
+ try {
91
+ const allAuth = JSON.parse(readFileSync(authPath, "utf-8"));
92
+ allAuth[providerName] = auth;
93
+ writeFileSync(authPath, JSON.stringify(allAuth));
94
+ }
95
+ catch { }
96
+ }
97
+ }
98
+ // Build headers
99
+ const requestInit = init ?? {};
100
+ const requestHeaders = new Headers();
101
+ if (input instanceof Request) {
102
+ input.headers.forEach((value, key) => requestHeaders.set(key, value));
103
+ }
104
+ if (requestInit.headers) {
105
+ if (requestInit.headers instanceof Headers) {
106
+ requestInit.headers.forEach((value, key) => requestHeaders.set(key, value));
107
+ }
108
+ else if (Array.isArray(requestInit.headers)) {
109
+ for (const pair of requestInit.headers) {
110
+ const [key, value] = pair;
111
+ if (typeof value !== "undefined")
112
+ requestHeaders.set(key, value);
113
+ }
114
+ }
115
+ else {
116
+ for (const [key, value] of Object.entries(requestInit.headers)) {
117
+ if (typeof value !== "undefined")
118
+ requestHeaders.set(key, String(value));
119
+ }
120
+ }
121
+ }
122
+ // Merge beta headers
123
+ const incomingBeta = requestHeaders.get("anthropic-beta") ?? "";
124
+ const incomingBetas = incomingBeta
125
+ .split(",")
126
+ .map((b) => b.trim())
127
+ .filter(Boolean);
128
+ const mergedBetas = [...new Set([...OAUTH_REQUIRED_BETAS, ...incomingBetas])].join(",");
129
+ requestHeaders.set("authorization", `Bearer ${auth.access}`);
130
+ requestHeaders.set("anthropic-beta", mergedBetas);
131
+ requestHeaders.set("user-agent", "claude-cli/2.1.2 (external, cli)");
132
+ requestHeaders.delete("x-api-key");
133
+ // Prefix tool names in request body
134
+ let body = requestInit.body;
135
+ if (body && typeof body === "string") {
136
+ try {
137
+ const parsed = JSON.parse(body);
138
+ if (parsed.tools && Array.isArray(parsed.tools)) {
139
+ parsed.tools = parsed.tools.map((tool) => ({
140
+ ...tool,
141
+ name: tool.name ? `${MCP_TOOL_PREFIX}${tool.name}` : tool.name,
142
+ }));
143
+ }
144
+ if (parsed.messages && Array.isArray(parsed.messages)) {
145
+ parsed.messages = parsed.messages.map((msg) => {
146
+ if (msg.content && Array.isArray(msg.content)) {
147
+ msg.content = msg.content.map((block) => {
148
+ if (block.type === "tool_use" && block.name) {
149
+ return { ...block, name: `${MCP_TOOL_PREFIX}${block.name}` };
150
+ }
151
+ return block;
152
+ });
153
+ }
154
+ return msg;
155
+ });
156
+ }
157
+ body = JSON.stringify(parsed);
158
+ }
159
+ catch { }
160
+ }
161
+ // Modify URL: add ?beta=true to /v1/messages
162
+ let requestInput = input;
163
+ try {
164
+ let requestUrl = null;
165
+ if (typeof input === "string" || input instanceof URL) {
166
+ requestUrl = new URL(input.toString());
167
+ }
168
+ else if (input instanceof Request) {
169
+ requestUrl = new URL(input.url);
170
+ }
171
+ if (requestUrl?.pathname === "/v1/messages" && !requestUrl.searchParams.has("beta")) {
172
+ requestUrl.searchParams.set("beta", "true");
173
+ requestInput =
174
+ input instanceof Request ? new Request(requestUrl.toString(), input) : requestUrl;
175
+ }
176
+ }
177
+ catch { }
178
+ const response = await fetch(requestInput, { ...requestInit, body, headers: requestHeaders });
179
+ // Strip mcp_ prefix from tool names in streaming response
180
+ if (response.body) {
181
+ const reader = response.body.getReader();
182
+ const decoder = new TextDecoder();
183
+ const encoder = new TextEncoder();
184
+ const stream = new ReadableStream({
185
+ async pull(controller) {
186
+ const { done, value } = await reader.read();
187
+ if (done) {
188
+ controller.close();
189
+ return;
190
+ }
191
+ let text = decoder.decode(value, { stream: true });
192
+ text = text.replace(/"name"\s*:\s*"mcp_([^"]+)"/g, '"name": "$1"');
193
+ controller.enqueue(encoder.encode(text));
194
+ },
195
+ });
196
+ return new Response(stream, {
197
+ status: response.status,
198
+ statusText: response.statusText,
199
+ headers: response.headers,
200
+ });
201
+ }
202
+ return response;
203
+ };
204
+ }
205
+ // --- Provider ---
206
+ export function createOpencodeAIProvider(providerName, auth, statePath) {
207
+ if (providerName === "anthropic") {
208
+ if (auth.type === "oauth") {
209
+ if (!statePath)
210
+ throw new Error("statePath is required for OAuth authentication");
211
+ return createAnthropic({
212
+ apiKey: "",
213
+ fetch: createOAuthFetch(statePath, providerName),
214
+ });
215
+ }
216
+ return createAnthropic({ apiKey: auth.key });
217
+ }
218
+ if (providerName === "openai") {
219
+ if (auth.type === "oauth") {
220
+ throw new Error("OpenAI does not support OAuth authentication. Use an API key instead.");
221
+ }
222
+ return createOpenAI({ apiKey: auth.key });
223
+ }
224
+ throw new Error(`Unsupported opencode provider: '${providerName}'. Supported providers: anthropic, openai`);
225
+ }
226
+ // --- Structured Output ---
227
+ export async function generateStructuredOutput(options) {
228
+ const auth = readOpencodeAuth(options.statePath, options.providerName);
229
+ const provider = createOpencodeAIProvider(options.providerName, auth, options.statePath);
230
+ const result = await generateText({
231
+ model: provider(options.modelId),
232
+ system: options.systemPrompt,
233
+ prompt: options.userPrompt,
234
+ output: Output.object({ schema: options.schema }),
235
+ temperature: options.temperature ?? 0.3,
236
+ });
237
+ return result.output;
238
+ }
@@ -176,6 +176,64 @@ function buildMarkdownContext(userPrompt, textResponses, toolCalls, latestMemory
176
176
  return sections.join("\n");
177
177
  }
178
178
  async function generateSummary(context, sessionID, userPrompt) {
179
+ // Opencode provider path (when opencodeProvider + opencodeModel configured)
180
+ if (CONFIG.opencodeProvider && CONFIG.opencodeModel) {
181
+ if (CONFIG.memoryModel) {
182
+ log("opencodeProvider takes precedence over memoryModel for auto-capture");
183
+ }
184
+ const { isProviderConnected, getStatePath, generateStructuredOutput } = await import("./ai/opencode-provider.js");
185
+ if (!isProviderConnected(CONFIG.opencodeProvider)) {
186
+ throw new Error(`opencode provider '${CONFIG.opencodeProvider}' is not connected. Check your opencode provider configuration.`);
187
+ }
188
+ const { detectLanguage, getLanguageName } = await import("./language-detector.js");
189
+ const targetLang = CONFIG.autoCaptureLanguage === "auto" || !CONFIG.autoCaptureLanguage
190
+ ? detectLanguage(userPrompt)
191
+ : CONFIG.autoCaptureLanguage;
192
+ const langName = getLanguageName(targetLang);
193
+ const systemPrompt = `You are a technical memory recorder for a software development project.
194
+
195
+ RULES:
196
+ 1. ONLY capture technical work (code, bugs, features, architecture, config)
197
+ 2. SKIP non-technical by returning type="skip"
198
+ 3. NO meta-commentary or behavior analysis
199
+ 4. Include specific file names, functions, technical details
200
+ 5. Generate 2-4 technical tags (e.g., "react", "auth", "bug-fix")
201
+ 6. You MUST write the summary in ${langName}.
202
+
203
+ FORMAT:
204
+ ## Request
205
+ [1-2 sentences: what was requested, in ${langName}]
206
+
207
+ ## Outcome
208
+ [1-2 sentences: what was done, include files/functions, in ${langName}]
209
+
210
+ SKIP if: greetings, casual chat, no code/decisions made
211
+ CAPTURE if: code changed, bug fixed, feature added, decision made`;
212
+ const aiPrompt = `${context}
213
+
214
+ Analyze this conversation. If it contains technical work (code, bugs, features, decisions), create a concise summary and relevant tags. If it's non-technical (greetings, casual chat, incomplete requests), return type="skip" with empty summary.`;
215
+ const { z } = await import("zod");
216
+ const schema = z.object({
217
+ summary: z.string(),
218
+ type: z.string(),
219
+ tags: z.array(z.string()),
220
+ });
221
+ const result = await generateStructuredOutput({
222
+ providerName: CONFIG.opencodeProvider,
223
+ modelId: CONFIG.opencodeModel,
224
+ statePath: getStatePath(),
225
+ systemPrompt,
226
+ userPrompt: aiPrompt,
227
+ schema,
228
+ temperature: CONFIG.memoryTemperature === false ? undefined : (CONFIG.memoryTemperature ?? 0.3),
229
+ });
230
+ return {
231
+ summary: result.summary,
232
+ type: result.type,
233
+ tags: (result.tags || []).map((t) => t.toLowerCase().trim()),
234
+ };
235
+ }
236
+ // Existing manual config path
179
237
  if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl) {
180
238
  throw new Error("External API not configured for auto-capture");
181
239
  }
@@ -105,6 +105,50 @@ Identify and ${existingProfile ? "update" : "create"}:
105
105
  ${existingProfile ? "Merge with existing profile, incrementing frequencies and updating confidence scores." : "Create initial profile with conservative confidence scores."}`;
106
106
  }
107
107
  async function analyzeUserProfile(context, existingProfile) {
108
+ if (CONFIG.opencodeProvider && CONFIG.opencodeModel) {
109
+ const { isProviderConnected, getStatePath, generateStructuredOutput } = await import("./ai/opencode-provider.js");
110
+ if (!isProviderConnected(CONFIG.opencodeProvider)) {
111
+ throw new Error(`opencode provider '${CONFIG.opencodeProvider}' is not connected. Check your opencode provider configuration.`);
112
+ }
113
+ const systemPrompt = `You are a user behavior analyst for a coding assistant.
114
+
115
+ Your task is to analyze user prompts and ${existingProfile ? "update" : "create"} a comprehensive user profile.
116
+
117
+ CRITICAL: Detect the language used by the user in their prompts. You MUST output all descriptions, categories, and text in the SAME language as the user's prompts.
118
+
119
+ Use the update_user_profile tool to save the ${existingProfile ? "updated" : "new"} profile.`;
120
+ const { z } = await import("zod");
121
+ const schema = z.object({
122
+ preferences: z.array(z.object({
123
+ category: z.string(),
124
+ description: z.string(),
125
+ confidence: z.number(),
126
+ evidence: z.array(z.string()),
127
+ })),
128
+ patterns: z.array(z.object({
129
+ category: z.string(),
130
+ description: z.string(),
131
+ })),
132
+ workflows: z.array(z.object({
133
+ description: z.string(),
134
+ steps: z.array(z.string()),
135
+ })),
136
+ });
137
+ const result = await generateStructuredOutput({
138
+ providerName: CONFIG.opencodeProvider,
139
+ modelId: CONFIG.opencodeModel,
140
+ statePath: getStatePath(),
141
+ systemPrompt,
142
+ userPrompt: context,
143
+ schema,
144
+ temperature: CONFIG.memoryTemperature === false ? undefined : (CONFIG.memoryTemperature ?? 0.3),
145
+ });
146
+ if (existingProfile) {
147
+ const existingData = JSON.parse(existingProfile.profileData);
148
+ return userProfileManager.mergeProfileData(existingData, result);
149
+ }
150
+ return result;
151
+ }
108
152
  if (!CONFIG.memoryModel || !CONFIG.memoryApiUrl) {
109
153
  log("User Profile Config Check Failed:", {
110
154
  memoryModel: CONFIG.memoryModel,
package/dist/web/app.js CHANGED
@@ -527,6 +527,21 @@ function deselectAll() {
527
527
  updateBulkActions();
528
528
  }
529
529
 
530
+ function selectAllCurrentPage() {
531
+ const checkboxes = document.querySelectorAll(".memory-checkbox");
532
+ if (checkboxes.length === 0) return;
533
+
534
+ checkboxes.forEach((cb) => {
535
+ cb.checked = true;
536
+ if (cb.dataset.id) {
537
+ state.selectedMemories.add(cb.dataset.id);
538
+ updateCardSelection(cb.dataset.id, true);
539
+ }
540
+ });
541
+
542
+ updateBulkActions();
543
+ }
544
+
530
545
  function editMemory(id) {
531
546
  const memory = state.memories.find((m) => m.id === id && m.type === "memory");
532
547
  if (!memory) return;
@@ -1164,6 +1179,7 @@ document.addEventListener("DOMContentLoaded", async () => {
1164
1179
  document.getElementById("next-page-bottom").addEventListener("click", () => changePage(1));
1165
1180
 
1166
1181
  document.getElementById("bulk-delete-btn").addEventListener("click", bulkDelete);
1182
+ document.getElementById("select-all-btn").addEventListener("click", selectAllCurrentPage);
1167
1183
  document.getElementById("deselect-all-btn").addEventListener("click", deselectAll);
1168
1184
 
1169
1185
  document.getElementById("cleanup-btn").addEventListener("click", runCleanup);
package/dist/web/i18n.js CHANGED
@@ -10,6 +10,7 @@ const translations = {
10
10
  "btn-cleanup": "Cleanup",
11
11
  "btn-deduplicate": "Deduplicate",
12
12
  "btn-delete-selected": "Delete Selected",
13
+ "btn-select-all": "Select Page",
13
14
  "btn-deselect-all": "Deselect All",
14
15
  "btn-add-memory": "Add Memory",
15
16
  "section-project": "└─ PROJECT MEMORIES ({count}) ──",
@@ -116,6 +117,7 @@ const translations = {
116
117
  "btn-cleanup": "清理",
117
118
  "btn-deduplicate": "去重",
118
119
  "btn-delete-selected": "删除选中",
120
+ "btn-select-all": "全选当前页",
119
121
  "btn-deselect-all": "取消全选",
120
122
  "btn-add-memory": "添加记忆",
121
123
  "section-project": "└─ 项目记忆 ({count}) ──",
@@ -77,6 +77,9 @@
77
77
 
78
78
  <div class="bulk-actions hidden" id="bulk-actions">
79
79
  <span id="selected-count" data-i18n="text-selected">0 selected</span>
80
+ <button id="select-all-btn" data-i18n="btn-select-all">
81
+ <i data-lucide="check-square" class="icon"></i> Select Page
82
+ </button>
80
83
  <button id="bulk-delete-btn" data-i18n="btn-delete-selected">
81
84
  <i data-lucide="trash-2" class="icon"></i> Delete Selected
82
85
  </button>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-mem",
3
- "version": "2.11.12",
3
+ "version": "2.12.1",
4
4
  "description": "OpenCode plugin that gives coding agents persistent memory using local vector database",
5
5
  "type": "module",
6
6
  "main": "dist/plugin.js",
@@ -33,11 +33,16 @@
33
33
  "access": "public"
34
34
  },
35
35
  "dependencies": {
36
+ "@ai-sdk/anthropic": "^3.0.58",
37
+ "@ai-sdk/openai": "^3.0.41",
36
38
  "@opencode-ai/plugin": "^1.0.162",
39
+ "@opencode-ai/sdk": "^1.2.26",
37
40
  "@xenova/transformers": "^2.17.2",
41
+ "ai": "^6.0.116",
38
42
  "franc-min": "^6.2.0",
39
43
  "iso-639-3": "^3.0.1",
40
- "usearch": "^2.21.4"
44
+ "usearch": "^2.21.4",
45
+ "zod": "^4.3.6"
41
46
  },
42
47
  "devDependencies": {
43
48
  "@types/bun": "^1.3.8",