opencode-mem 2.11.12 → 2.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/config.d.ts CHANGED
@@ -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;
@@ -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":"AAqcA,eAAO,MAAM,MAAM;;;;;;;;;;;;;;;;;oBAwBb,aAAa,GACb,kBAAkB,GAClB,WAAW;;;;;;;;mBASX,eAAe,GACf,SAAS,GACT,YAAY;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAoCV,OAAO,GACP,QAAQ;;CAEf,CAAC;AAEF,wBAAgB,YAAY,IAAI,OAAO,CAEtC"}
package/dist/config.js CHANGED
@@ -142,12 +142,30 @@ const CONFIG_TEMPLATE = `{
142
142
  // Automatically detect and remove duplicate memories
143
143
  "deduplicationEnabled": true,
144
144
 
145
- // Similarity threshold (0-1) for detecting duplicates (higher = stricter)
146
- "deduplicationSimilarityThreshold": 0.90,
147
-
148
- // ============================================
149
- // Auto-Capture Settings (REQUIRES EXTERNAL API)
150
- // ============================================
145
+ // Similarity threshold (0-1) for detecting duplicates (higher = stricter)
146
+ "deduplicationSimilarityThreshold": 0.90,
147
+
148
+ // ============================================
149
+ // OpenCode Provider Settings (RECOMMENDED)
150
+ // ============================================
151
+
152
+ // Use opencode's already-configured providers for auto-capture and user profile learning.
153
+ // When set, no separate API key is needed — uses your existing opencode authentication
154
+ // (including Claude Pro/Max plans via OAuth, or any API key configured in opencode).
155
+ //
156
+ // If NOT set, falls back to the manual config (memoryApiKey/memoryApiUrl/memoryModel below).
157
+ //
158
+ // Examples:
159
+ // Anthropic (OAuth/API key): "opencodeProvider": "anthropic", "opencodeModel": "claude-haiku-4-5-20251001"
160
+ // OpenAI (API key): "opencodeProvider": "openai", "opencodeModel": "gpt-4o-mini"
161
+ //
162
+ // The provider name must match a connected provider in opencode (check with: opencode providers list)
163
+ // "opencodeProvider": "anthropic",
164
+ // "opencodeModel": "claude-haiku-4-5-20251001",
165
+
166
+ // ============================================
167
+ // Auto-Capture Settings (REQUIRES EXTERNAL API)
168
+ // ============================================
151
169
 
152
170
  // IMPORTANT: Auto-capture ONLY works with external API
153
171
  // It runs in background without blocking your main session
@@ -355,6 +373,8 @@ export const CONFIG = {
355
373
  memoryApiKey: resolveSecretValue(fileConfig.memoryApiKey),
356
374
  memoryTemperature: fileConfig.memoryTemperature,
357
375
  memoryExtraParams: fileConfig.memoryExtraParams,
376
+ opencodeProvider: fileConfig.opencodeProvider,
377
+ opencodeModel: fileConfig.opencodeModel,
358
378
  vectorBackend: (fileConfig.vectorBackend ?? "usearch-first"),
359
379
  aiSessionRetentionDays: fileConfig.aiSessionRetentionDays ?? DEFAULTS.aiSessionRetentionDays,
360
380
  webServerEnabled: fileConfig.webServerEnabled ?? DEFAULTS.webServerEnabled,
@@ -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,MA+b/B,CAAC"}
package/dist/index.js CHANGED
@@ -10,6 +10,7 @@ import { startWebServer, WebServer } from "./services/web-server.js";
10
10
  import { isConfigured, CONFIG } 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;
15
16
  const tags = getTags(directory);
@@ -27,6 +28,23 @@ export const OpenCodeMemPlugin = async (ctx) => {
27
28
  log("Plugin warmup failed", { error: String(error) });
28
29
  }
29
30
  }
31
+ // Wire opencode state path and provider list — fire-and-forget to avoid blocking init
32
+ // These calls can hang if opencode isn't fully bootstrapped yet
33
+ (async () => {
34
+ try {
35
+ const pathResult = await ctx.client.path.get();
36
+ if (pathResult.data?.state) {
37
+ setStatePath(pathResult.data.state);
38
+ }
39
+ const providerResult = await ctx.client.provider.list();
40
+ if (providerResult.data?.connected) {
41
+ setConnectedProviders(providerResult.data.connected);
42
+ }
43
+ }
44
+ catch (error) {
45
+ log("Failed to initialize opencode provider state", { error: String(error) });
46
+ }
47
+ })();
30
48
  if (CONFIG.webServerEnabled) {
31
49
  startWebServer({
32
50
  port: CONFIG.webServerPort,
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-mem",
3
- "version": "2.11.12",
3
+ "version": "2.12.0",
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",