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 +2 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +26 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +18 -0
- package/dist/services/ai/opencode-provider.d.ts +30 -0
- package/dist/services/ai/opencode-provider.d.ts.map +1 -0
- package/dist/services/ai/opencode-provider.js +238 -0
- package/dist/services/auto-capture.js +58 -0
- package/dist/services/user-memory-learning.js +44 -0
- package/package.json +7 -2
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;
|
package/dist/config.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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,
|
package/dist/index.d.ts.map
CHANGED
|
@@ -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;
|
|
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.
|
|
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",
|