@haisto/opencode-mem 2.14.3-beta.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/README.md +165 -0
- package/dist/config.d.ts +62 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +457 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +486 -0
- package/dist/plugin.d.ts +9 -0
- package/dist/plugin.d.ts.map +1 -0
- package/dist/plugin.js +5 -0
- package/dist/services/ai/ai-provider-factory.d.ts +8 -0
- package/dist/services/ai/ai-provider-factory.d.ts.map +1 -0
- package/dist/services/ai/ai-provider-factory.js +28 -0
- package/dist/services/ai/opencode-provider.d.ts +36 -0
- package/dist/services/ai/opencode-provider.d.ts.map +1 -0
- package/dist/services/ai/opencode-provider.js +92 -0
- package/dist/services/ai/provider-config.d.ts +17 -0
- package/dist/services/ai/provider-config.d.ts.map +1 -0
- package/dist/services/ai/provider-config.js +14 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts +12 -0
- package/dist/services/ai/providers/anthropic-messages.d.ts.map +1 -0
- package/dist/services/ai/providers/anthropic-messages.js +184 -0
- package/dist/services/ai/providers/base-provider.d.ts +25 -0
- package/dist/services/ai/providers/base-provider.d.ts.map +1 -0
- package/dist/services/ai/providers/base-provider.js +23 -0
- package/dist/services/ai/providers/google-gemini.d.ts +16 -0
- package/dist/services/ai/providers/google-gemini.d.ts.map +1 -0
- package/dist/services/ai/providers/google-gemini.js +228 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts +14 -0
- package/dist/services/ai/providers/openai-chat-completion.d.ts.map +1 -0
- package/dist/services/ai/providers/openai-chat-completion.js +318 -0
- package/dist/services/ai/providers/openai-responses.d.ts +14 -0
- package/dist/services/ai/providers/openai-responses.d.ts.map +1 -0
- package/dist/services/ai/providers/openai-responses.js +182 -0
- package/dist/services/ai/session/ai-session-manager.d.ts +21 -0
- package/dist/services/ai/session/ai-session-manager.d.ts.map +1 -0
- package/dist/services/ai/session/ai-session-manager.js +166 -0
- package/dist/services/ai/session/session-types.d.ts +43 -0
- package/dist/services/ai/session/session-types.d.ts.map +1 -0
- package/dist/services/ai/session/session-types.js +1 -0
- package/dist/services/ai/tools/tool-schema.d.ts +41 -0
- package/dist/services/ai/tools/tool-schema.d.ts.map +1 -0
- package/dist/services/ai/tools/tool-schema.js +24 -0
- package/dist/services/ai/validators/user-profile-validator.d.ts +13 -0
- package/dist/services/ai/validators/user-profile-validator.d.ts.map +1 -0
- package/dist/services/ai/validators/user-profile-validator.js +111 -0
- package/dist/services/api-handlers.d.ts +164 -0
- package/dist/services/api-handlers.d.ts.map +1 -0
- package/dist/services/api-handlers.js +927 -0
- package/dist/services/auto-capture.d.ts +3 -0
- package/dist/services/auto-capture.d.ts.map +1 -0
- package/dist/services/auto-capture.js +309 -0
- package/dist/services/cleanup-service.d.ts +23 -0
- package/dist/services/cleanup-service.d.ts.map +1 -0
- package/dist/services/cleanup-service.js +102 -0
- package/dist/services/client.d.ts +119 -0
- package/dist/services/client.d.ts.map +1 -0
- package/dist/services/client.js +257 -0
- package/dist/services/context.d.ts +11 -0
- package/dist/services/context.d.ts.map +1 -0
- package/dist/services/context.js +34 -0
- package/dist/services/deduplication-service.d.ts +30 -0
- package/dist/services/deduplication-service.d.ts.map +1 -0
- package/dist/services/deduplication-service.js +124 -0
- package/dist/services/embedding.d.ts +15 -0
- package/dist/services/embedding.d.ts.map +1 -0
- package/dist/services/embedding.js +127 -0
- package/dist/services/jsonc.d.ts +7 -0
- package/dist/services/jsonc.d.ts.map +1 -0
- package/dist/services/jsonc.js +76 -0
- package/dist/services/language-detector.d.ts +3 -0
- package/dist/services/language-detector.d.ts.map +1 -0
- package/dist/services/language-detector.js +33 -0
- package/dist/services/logger.d.ts +11 -0
- package/dist/services/logger.d.ts.map +1 -0
- package/dist/services/logger.js +97 -0
- package/dist/services/migration-service.d.ts +42 -0
- package/dist/services/migration-service.d.ts.map +1 -0
- package/dist/services/migration-service.js +250 -0
- package/dist/services/privacy.d.ts +3 -0
- package/dist/services/privacy.d.ts.map +1 -0
- package/dist/services/privacy.js +7 -0
- package/dist/services/secret-resolver.d.ts +2 -0
- package/dist/services/secret-resolver.d.ts.map +1 -0
- package/dist/services/secret-resolver.js +55 -0
- package/dist/services/sqlite/connection-manager.d.ts +13 -0
- package/dist/services/sqlite/connection-manager.d.ts.map +1 -0
- package/dist/services/sqlite/connection-manager.js +74 -0
- package/dist/services/sqlite/shard-manager.d.ts +23 -0
- package/dist/services/sqlite/shard-manager.d.ts.map +1 -0
- package/dist/services/sqlite/shard-manager.js +288 -0
- package/dist/services/sqlite/sqlite-bootstrap.d.ts +2 -0
- package/dist/services/sqlite/sqlite-bootstrap.d.ts.map +1 -0
- package/dist/services/sqlite/sqlite-bootstrap.js +8 -0
- package/dist/services/sqlite/types.d.ts +42 -0
- package/dist/services/sqlite/types.d.ts.map +1 -0
- package/dist/services/sqlite/types.js +1 -0
- package/dist/services/sqlite/vector-search.d.ts +29 -0
- package/dist/services/sqlite/vector-search.d.ts.map +1 -0
- package/dist/services/sqlite/vector-search.js +279 -0
- package/dist/services/tags.d.ts +24 -0
- package/dist/services/tags.d.ts.map +1 -0
- package/dist/services/tags.js +145 -0
- package/dist/services/user-memory-learning.d.ts +3 -0
- package/dist/services/user-memory-learning.d.ts.map +1 -0
- package/dist/services/user-memory-learning.js +235 -0
- package/dist/services/user-profile/profile-context.d.ts +2 -0
- package/dist/services/user-profile/profile-context.d.ts.map +1 -0
- package/dist/services/user-profile/profile-context.js +40 -0
- package/dist/services/user-profile/profile-utils.d.ts +3 -0
- package/dist/services/user-profile/profile-utils.d.ts.map +1 -0
- package/dist/services/user-profile/profile-utils.js +45 -0
- package/dist/services/user-profile/types.d.ts +46 -0
- package/dist/services/user-profile/types.d.ts.map +1 -0
- package/dist/services/user-profile/types.js +1 -0
- package/dist/services/user-profile/user-profile-manager.d.ts +23 -0
- package/dist/services/user-profile/user-profile-manager.d.ts.map +1 -0
- package/dist/services/user-profile/user-profile-manager.js +337 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts +41 -0
- package/dist/services/user-prompt/user-prompt-manager.d.ts.map +1 -0
- package/dist/services/user-prompt/user-prompt-manager.js +192 -0
- package/dist/services/vector-backends/backend-factory.d.ts +3 -0
- package/dist/services/vector-backends/backend-factory.d.ts.map +1 -0
- package/dist/services/vector-backends/backend-factory.js +104 -0
- package/dist/services/vector-backends/exact-scan-backend.d.ts +39 -0
- package/dist/services/vector-backends/exact-scan-backend.d.ts.map +1 -0
- package/dist/services/vector-backends/exact-scan-backend.js +63 -0
- package/dist/services/vector-backends/types.d.ts +51 -0
- package/dist/services/vector-backends/types.d.ts.map +1 -0
- package/dist/services/vector-backends/types.js +1 -0
- package/dist/services/vector-backends/usearch-backend.d.ts +47 -0
- package/dist/services/vector-backends/usearch-backend.d.ts.map +1 -0
- package/dist/services/vector-backends/usearch-backend.js +174 -0
- package/dist/services/web-server-worker.d.ts +2 -0
- package/dist/services/web-server-worker.d.ts.map +1 -0
- package/dist/services/web-server-worker.js +283 -0
- package/dist/services/web-server.d.ts +31 -0
- package/dist/services/web-server.d.ts.map +1 -0
- package/dist/services/web-server.js +356 -0
- package/dist/types/index.d.ts +19 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +1 -0
- package/dist/web/app.d.ts +2 -0
- package/dist/web/app.d.ts.map +1 -0
- package/dist/web/app.js +1238 -0
- package/dist/web/favicon.ico +0 -0
- package/dist/web/i18n.d.ts +2 -0
- package/dist/web/i18n.d.ts.map +1 -0
- package/dist/web/i18n.js +312 -0
- package/dist/web/index.html +293 -0
- package/dist/web/styles.css +1786 -0
- package/package.json +78 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
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,MA4hB/B,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,486 @@
|
|
|
1
|
+
import { tool } from "@opencode-ai/plugin";
|
|
2
|
+
import { memoryClient } from "./services/client.js";
|
|
3
|
+
import { formatContextForPrompt } from "./services/context.js";
|
|
4
|
+
import { getTags } from "./services/tags.js";
|
|
5
|
+
import { stripPrivateContent, isFullyPrivate } from "./services/privacy.js";
|
|
6
|
+
import { performAutoCapture } from "./services/auto-capture.js";
|
|
7
|
+
import { performUserProfileLearning } from "./services/user-memory-learning.js";
|
|
8
|
+
import { userPromptManager } from "./services/user-prompt/user-prompt-manager.js";
|
|
9
|
+
import { startWebServer, WebServer } from "./services/web-server.js";
|
|
10
|
+
import { isConfigured, CONFIG, initConfig } from "./config.js";
|
|
11
|
+
import { log } from "./services/logger.js";
|
|
12
|
+
import { getLanguageName } from "./services/language-detector.js";
|
|
13
|
+
export const OpenCodeMemPlugin = async (ctx) => {
|
|
14
|
+
const { directory } = ctx;
|
|
15
|
+
initConfig(directory);
|
|
16
|
+
const tags = getTags(directory);
|
|
17
|
+
let webServer = null;
|
|
18
|
+
let idleTimeout = null;
|
|
19
|
+
if (!isConfigured()) {
|
|
20
|
+
}
|
|
21
|
+
const GLOBAL_PLUGIN_WARMUP_KEY = Symbol.for("opencode-mem.plugin.warmedup");
|
|
22
|
+
if (!globalThis[GLOBAL_PLUGIN_WARMUP_KEY] && isConfigured()) {
|
|
23
|
+
// Fire-and-forget: warmup is slow (embedding model load + index rebuild).
|
|
24
|
+
// Awaiting it here serializes opencode's plugin loader and starves the TUI,
|
|
25
|
+
// which gave the symptom "opencode hangs ~70s then disconnects on startup".
|
|
26
|
+
(async () => {
|
|
27
|
+
try {
|
|
28
|
+
await memoryClient.warmup();
|
|
29
|
+
globalThis[GLOBAL_PLUGIN_WARMUP_KEY] = true;
|
|
30
|
+
}
|
|
31
|
+
catch (error) {
|
|
32
|
+
log("Plugin warmup failed", { error: String(error) });
|
|
33
|
+
}
|
|
34
|
+
})();
|
|
35
|
+
}
|
|
36
|
+
(async () => {
|
|
37
|
+
try {
|
|
38
|
+
const { setConnectedProviders, setV2Client, createV2Client } = await import("./services/ai/opencode-provider.js");
|
|
39
|
+
setV2Client(createV2Client(ctx.serverUrl));
|
|
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
|
+
})();
|
|
49
|
+
if (CONFIG.webServerEnabled) {
|
|
50
|
+
startWebServer({
|
|
51
|
+
port: CONFIG.webServerPort,
|
|
52
|
+
host: CONFIG.webServerHost,
|
|
53
|
+
enabled: CONFIG.webServerEnabled,
|
|
54
|
+
})
|
|
55
|
+
.then((server) => {
|
|
56
|
+
webServer = server;
|
|
57
|
+
const url = webServer.getUrl();
|
|
58
|
+
webServer.setOnTakeoverCallback(async () => {
|
|
59
|
+
if (ctx.client?.tui) {
|
|
60
|
+
ctx.client.tui
|
|
61
|
+
.showToast({
|
|
62
|
+
body: {
|
|
63
|
+
title: "Memory Explorer",
|
|
64
|
+
message: "Took over web server ownership",
|
|
65
|
+
variant: "success",
|
|
66
|
+
duration: 3000,
|
|
67
|
+
},
|
|
68
|
+
})
|
|
69
|
+
.catch(() => { });
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
if (webServer.isServerOwner()) {
|
|
73
|
+
if (ctx.client?.tui) {
|
|
74
|
+
ctx.client.tui
|
|
75
|
+
.showToast({
|
|
76
|
+
body: {
|
|
77
|
+
title: "Memory Explorer",
|
|
78
|
+
message: `Web UI started at ${url}`,
|
|
79
|
+
variant: "success",
|
|
80
|
+
duration: 5000,
|
|
81
|
+
},
|
|
82
|
+
})
|
|
83
|
+
.catch(() => { });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
if (ctx.client?.tui) {
|
|
88
|
+
ctx.client.tui
|
|
89
|
+
.showToast({
|
|
90
|
+
body: {
|
|
91
|
+
title: "Memory Explorer",
|
|
92
|
+
message: `Web UI available at ${url}`,
|
|
93
|
+
variant: "info",
|
|
94
|
+
duration: 3000,
|
|
95
|
+
},
|
|
96
|
+
})
|
|
97
|
+
.catch(() => { });
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
})
|
|
101
|
+
.catch((error) => {
|
|
102
|
+
log("Web server failed to start", { error: String(error) });
|
|
103
|
+
if (ctx.client?.tui) {
|
|
104
|
+
ctx.client.tui
|
|
105
|
+
.showToast({
|
|
106
|
+
body: {
|
|
107
|
+
title: "Memory Explorer Error",
|
|
108
|
+
message: `Failed to start: ${String(error)}`,
|
|
109
|
+
variant: "error",
|
|
110
|
+
duration: 5000,
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
.catch(() => { });
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const shutdownHandler = async () => {
|
|
118
|
+
try {
|
|
119
|
+
if (webServer) {
|
|
120
|
+
await webServer.stop();
|
|
121
|
+
}
|
|
122
|
+
memoryClient.close();
|
|
123
|
+
process.exit(0);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
log("Shutdown error", { error: String(error) });
|
|
127
|
+
process.exit(1);
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
process.on("SIGINT", shutdownHandler);
|
|
131
|
+
process.on("SIGTERM", shutdownHandler);
|
|
132
|
+
return {
|
|
133
|
+
"chat.message": async (input, output) => {
|
|
134
|
+
if (!isConfigured() || !CONFIG.chatMessage.enabled)
|
|
135
|
+
return;
|
|
136
|
+
try {
|
|
137
|
+
const textParts = output.parts.filter((p) => p.type === "text");
|
|
138
|
+
if (textParts.length === 0)
|
|
139
|
+
return;
|
|
140
|
+
const userMessage = textParts.map((p) => p.text).join("\n");
|
|
141
|
+
if (!userMessage.trim())
|
|
142
|
+
return;
|
|
143
|
+
userPromptManager.savePrompt(input.sessionID, output.message.id, directory, userMessage);
|
|
144
|
+
const messagesResponse = await ctx.client.session.messages({
|
|
145
|
+
path: { id: input.sessionID },
|
|
146
|
+
});
|
|
147
|
+
const messages = messagesResponse.data || [];
|
|
148
|
+
const hasNonSyntheticUserMessages = messages.some((m) => m.info.role === "user" &&
|
|
149
|
+
!m.parts.every((p) => p.type !== "text" || p.synthetic === true));
|
|
150
|
+
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
151
|
+
const isAfterCompaction = lastMessage?.info?.summary === true;
|
|
152
|
+
const shouldInject = CONFIG.chatMessage.injectOn === "always" ||
|
|
153
|
+
!hasNonSyntheticUserMessages ||
|
|
154
|
+
(isAfterCompaction &&
|
|
155
|
+
messages.filter((m) => m.info.role === "user" &&
|
|
156
|
+
!m.parts.every((p) => p.type !== "text" || p.synthetic === true)).length === 1);
|
|
157
|
+
if (!shouldInject)
|
|
158
|
+
return;
|
|
159
|
+
const listResult = await memoryClient.listMemories(tags.project.tag, CONFIG.chatMessage.maxMemories);
|
|
160
|
+
let memories = listResult.success ? listResult.memories : [];
|
|
161
|
+
if (CONFIG.chatMessage.excludeCurrentSession) {
|
|
162
|
+
memories = memories.filter((m) => m.metadata?.sessionID !== input.sessionID);
|
|
163
|
+
}
|
|
164
|
+
if (CONFIG.chatMessage.maxAgeDays) {
|
|
165
|
+
const cutoffDate = Date.now() - CONFIG.chatMessage.maxAgeDays * 86400000;
|
|
166
|
+
memories = memories.filter((m) => new Date(m.createdAt).getTime() > cutoffDate);
|
|
167
|
+
}
|
|
168
|
+
if (memories.length === 0)
|
|
169
|
+
return;
|
|
170
|
+
const projectMemories = {
|
|
171
|
+
results: memories.map((m) => ({
|
|
172
|
+
similarity: 1.0,
|
|
173
|
+
memory: m.summary,
|
|
174
|
+
})),
|
|
175
|
+
total: memories.length,
|
|
176
|
+
timing: 0,
|
|
177
|
+
};
|
|
178
|
+
const userId = tags.user.userEmail || null;
|
|
179
|
+
const memoryContext = formatContextForPrompt(userId, projectMemories);
|
|
180
|
+
if (memoryContext) {
|
|
181
|
+
const contextPart = {
|
|
182
|
+
id: `prt-memory-context-${Date.now()}`,
|
|
183
|
+
sessionID: input.sessionID,
|
|
184
|
+
messageID: output.message.id,
|
|
185
|
+
type: "text",
|
|
186
|
+
text: memoryContext,
|
|
187
|
+
synthetic: true,
|
|
188
|
+
};
|
|
189
|
+
output.parts.unshift(contextPart);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
log("chat.message: ERROR", { error: String(error) });
|
|
194
|
+
if (ctx.client?.tui && CONFIG.showErrorToasts) {
|
|
195
|
+
await ctx.client.tui
|
|
196
|
+
.showToast({
|
|
197
|
+
body: {
|
|
198
|
+
title: "Memory System Error",
|
|
199
|
+
message: String(error),
|
|
200
|
+
variant: "error",
|
|
201
|
+
duration: 5000,
|
|
202
|
+
},
|
|
203
|
+
})
|
|
204
|
+
.catch(() => { });
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
},
|
|
208
|
+
tool: {
|
|
209
|
+
memory: tool({
|
|
210
|
+
description: `Your persistent brain. Before answering, always search(${getLanguageName(CONFIG.autoCaptureLanguage || "en")}) — you may have relevant knowledge. Save key info, decisions, and user preferences proactively. Search with technical keywords/tags (tags rank highest). Use 'profile' to store user preferences. Scope: project or all-projects.`,
|
|
211
|
+
args: {
|
|
212
|
+
mode: tool.schema.enum(["add", "search", "profile", "list", "forget", "help"]).optional(),
|
|
213
|
+
content: tool.schema.string().optional(),
|
|
214
|
+
query: tool.schema.string().optional(),
|
|
215
|
+
tags: tool.schema.string().optional(),
|
|
216
|
+
type: tool.schema.string().optional(),
|
|
217
|
+
memoryId: tool.schema.string().optional(),
|
|
218
|
+
limit: tool.schema.number().optional(),
|
|
219
|
+
scope: tool.schema.enum(["project", "all-projects"]).optional(),
|
|
220
|
+
},
|
|
221
|
+
async execute(args, toolCtx) {
|
|
222
|
+
if (!isConfigured()) {
|
|
223
|
+
return JSON.stringify({
|
|
224
|
+
success: false,
|
|
225
|
+
error: "Memory system not configured properly.",
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
const needsWarmup = !(await memoryClient.isReady());
|
|
229
|
+
if (needsWarmup) {
|
|
230
|
+
return JSON.stringify({ success: false, error: "Memory system is initializing." });
|
|
231
|
+
}
|
|
232
|
+
const mode = args.mode || "help";
|
|
233
|
+
const langName = getLanguageName(CONFIG.autoCaptureLanguage || "en");
|
|
234
|
+
try {
|
|
235
|
+
switch (mode) {
|
|
236
|
+
case "help":
|
|
237
|
+
return JSON.stringify({
|
|
238
|
+
success: true,
|
|
239
|
+
message: "Memory System Usage Guide",
|
|
240
|
+
commands: [
|
|
241
|
+
{
|
|
242
|
+
command: "add",
|
|
243
|
+
description: `Store new memory (MATCH USER LANGUAGE: ${langName})`,
|
|
244
|
+
args: ["content", "type?", "tags?"],
|
|
245
|
+
},
|
|
246
|
+
{
|
|
247
|
+
command: "search",
|
|
248
|
+
description: `Search memories via keywords (MATCH USER LANGUAGE: ${langName})`,
|
|
249
|
+
args: ["query"],
|
|
250
|
+
},
|
|
251
|
+
{
|
|
252
|
+
command: "profile",
|
|
253
|
+
description: "View user profile or save an explicit preference (provide content to write)",
|
|
254
|
+
args: ["content?"],
|
|
255
|
+
},
|
|
256
|
+
{ command: "list", description: "List recent memories", args: ["limit?"] },
|
|
257
|
+
{ command: "forget", description: "Remove memory", args: ["memoryId"] },
|
|
258
|
+
],
|
|
259
|
+
tagGuidance: "Use technical keywords for search. Tags rank highest.",
|
|
260
|
+
});
|
|
261
|
+
case "add":
|
|
262
|
+
if (!args.content)
|
|
263
|
+
return JSON.stringify({ success: false, error: "content required" });
|
|
264
|
+
const sanitizedContent = stripPrivateContent(args.content);
|
|
265
|
+
if (isFullyPrivate(args.content))
|
|
266
|
+
return JSON.stringify({ success: false, error: "Private content blocked" });
|
|
267
|
+
const tagInfo = tags.project;
|
|
268
|
+
const parsedTags = args.tags
|
|
269
|
+
? args.tags.split(",").map((t) => t.trim().toLowerCase())
|
|
270
|
+
: undefined;
|
|
271
|
+
const result = await memoryClient.addMemory(sanitizedContent, tagInfo.tag, {
|
|
272
|
+
type: args.type,
|
|
273
|
+
tags: parsedTags,
|
|
274
|
+
displayName: tagInfo.displayName,
|
|
275
|
+
userName: tagInfo.userName,
|
|
276
|
+
userEmail: tagInfo.userEmail,
|
|
277
|
+
projectPath: tagInfo.projectPath,
|
|
278
|
+
projectName: tagInfo.projectName,
|
|
279
|
+
gitRepoUrl: tagInfo.gitRepoUrl,
|
|
280
|
+
});
|
|
281
|
+
return JSON.stringify({
|
|
282
|
+
success: result.success,
|
|
283
|
+
message: `Memory added`,
|
|
284
|
+
id: result.id,
|
|
285
|
+
tags: parsedTags,
|
|
286
|
+
});
|
|
287
|
+
case "search":
|
|
288
|
+
if (!args.query)
|
|
289
|
+
return JSON.stringify({ success: false, error: "query required" });
|
|
290
|
+
const searchRes = await memoryClient.searchMemories(args.query, tags.project.tag, args.scope ?? CONFIG.memory.defaultScope);
|
|
291
|
+
if (!searchRes.success)
|
|
292
|
+
return JSON.stringify({ success: false, error: searchRes.error });
|
|
293
|
+
return formatSearchResults(args.query, searchRes, args.limit);
|
|
294
|
+
case "profile": {
|
|
295
|
+
if (args.query) {
|
|
296
|
+
return JSON.stringify({
|
|
297
|
+
success: false,
|
|
298
|
+
error: "query is not valid for profile mode. Use content to write a preference or omit all args to read.",
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
const { userProfileManager } = await import("./services/user-profile/user-profile-manager.js");
|
|
302
|
+
const userId = tags.user.userEmail || "unknown";
|
|
303
|
+
// --- WRITE: explicit preference ---
|
|
304
|
+
if (args.content !== undefined) {
|
|
305
|
+
const trimmed = args.content.trim();
|
|
306
|
+
if (!trimmed) {
|
|
307
|
+
return JSON.stringify({ success: false, error: "content must not be blank" });
|
|
308
|
+
}
|
|
309
|
+
if (!tags.user.userEmail) {
|
|
310
|
+
return JSON.stringify({
|
|
311
|
+
success: false,
|
|
312
|
+
error: "Cannot save profile preference because no user email could be resolved. Configure userEmailOverride or git user.email.",
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
const sanitizedContent = stripPrivateContent(trimmed);
|
|
316
|
+
const hasNonPrivateContent = sanitizedContent.replace(/\[REDACTED\]/g, "").trim().length > 0;
|
|
317
|
+
if (isFullyPrivate(trimmed) || !hasNonPrivateContent) {
|
|
318
|
+
return JSON.stringify({ success: false, error: "Private content blocked" });
|
|
319
|
+
}
|
|
320
|
+
const newPreference = {
|
|
321
|
+
category: "explicit",
|
|
322
|
+
description: sanitizedContent,
|
|
323
|
+
confidence: 1.0,
|
|
324
|
+
evidence: ["manual-write"],
|
|
325
|
+
lastUpdated: Date.now(),
|
|
326
|
+
};
|
|
327
|
+
const existingProfile = userProfileManager.getActiveProfile(userId);
|
|
328
|
+
if (existingProfile) {
|
|
329
|
+
const existingData = JSON.parse(existingProfile.profileData);
|
|
330
|
+
const mergedData = userProfileManager.mergeProfileData(existingData, {
|
|
331
|
+
preferences: [newPreference],
|
|
332
|
+
});
|
|
333
|
+
userProfileManager.updateProfile(existingProfile.id, mergedData, 0, `Explicit preference added: ${sanitizedContent.slice(0, 80)}`);
|
|
334
|
+
return JSON.stringify({
|
|
335
|
+
success: true,
|
|
336
|
+
message: "Preference saved to profile",
|
|
337
|
+
});
|
|
338
|
+
}
|
|
339
|
+
else {
|
|
340
|
+
userProfileManager.createProfile(userId, tags.user.displayName || userId, tags.user.userName || userId, tags.user.userEmail || userId, { preferences: [newPreference], patterns: [], workflows: [] }, 0);
|
|
341
|
+
return JSON.stringify({
|
|
342
|
+
success: true,
|
|
343
|
+
message: "Profile created with preference",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
// --- READ: no content provided ---
|
|
348
|
+
const profile = userProfileManager.getActiveProfile(userId);
|
|
349
|
+
if (!profile)
|
|
350
|
+
return JSON.stringify({ success: true, profile: null });
|
|
351
|
+
const pData = JSON.parse(profile.profileData);
|
|
352
|
+
return JSON.stringify({
|
|
353
|
+
success: true,
|
|
354
|
+
profile: {
|
|
355
|
+
...pData,
|
|
356
|
+
version: profile.version,
|
|
357
|
+
lastAnalyzed: profile.lastAnalyzedAt,
|
|
358
|
+
},
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
case "list":
|
|
362
|
+
const listRes = await memoryClient.listMemories(tags.project.tag, args.limit || 20, args.scope ?? CONFIG.memory.defaultScope);
|
|
363
|
+
if (!listRes.success)
|
|
364
|
+
return JSON.stringify({ success: false, error: listRes.error });
|
|
365
|
+
return JSON.stringify({
|
|
366
|
+
success: true,
|
|
367
|
+
count: listRes.memories?.length,
|
|
368
|
+
memories: listRes.memories?.map((m) => ({
|
|
369
|
+
id: m.id,
|
|
370
|
+
content: m.summary,
|
|
371
|
+
createdAt: m.createdAt,
|
|
372
|
+
})),
|
|
373
|
+
});
|
|
374
|
+
case "forget":
|
|
375
|
+
if (!args.memoryId)
|
|
376
|
+
return JSON.stringify({ success: false, error: "memoryId required" });
|
|
377
|
+
const delRes = await memoryClient.deleteMemory(args.memoryId);
|
|
378
|
+
return JSON.stringify({ success: delRes.success, message: `Memory removed` });
|
|
379
|
+
default:
|
|
380
|
+
return JSON.stringify({ success: false, error: `Unknown mode: ${mode}` });
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
return JSON.stringify({ success: false, error: String(error) });
|
|
385
|
+
}
|
|
386
|
+
},
|
|
387
|
+
}),
|
|
388
|
+
},
|
|
389
|
+
event: async (input) => {
|
|
390
|
+
const event = input.event;
|
|
391
|
+
if (event.type === "session.idle") {
|
|
392
|
+
if (!isConfigured() || !CONFIG.autoCaptureEnabled)
|
|
393
|
+
return;
|
|
394
|
+
const sessionID = event.properties?.sessionID;
|
|
395
|
+
if (!sessionID)
|
|
396
|
+
return;
|
|
397
|
+
if (idleTimeout)
|
|
398
|
+
clearTimeout(idleTimeout);
|
|
399
|
+
idleTimeout = setTimeout(async () => {
|
|
400
|
+
try {
|
|
401
|
+
await performAutoCapture(ctx, sessionID, directory);
|
|
402
|
+
if (webServer?.isServerOwner()) {
|
|
403
|
+
await performUserProfileLearning(ctx, directory);
|
|
404
|
+
const { cleanupService } = await import("./services/cleanup-service.js");
|
|
405
|
+
if (await cleanupService.shouldRunCleanup())
|
|
406
|
+
await cleanupService.runCleanup();
|
|
407
|
+
const { connectionManager } = await import("./services/sqlite/connection-manager.js");
|
|
408
|
+
connectionManager.checkpointAll();
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch (error) {
|
|
412
|
+
log("Idle processing error", { error: String(error) });
|
|
413
|
+
}
|
|
414
|
+
finally {
|
|
415
|
+
idleTimeout = null;
|
|
416
|
+
}
|
|
417
|
+
}, 10000);
|
|
418
|
+
}
|
|
419
|
+
if (event.type === "session.compacted") {
|
|
420
|
+
if (!isConfigured() || !CONFIG.compaction.enabled)
|
|
421
|
+
return;
|
|
422
|
+
const sessionID = event.properties?.sessionID;
|
|
423
|
+
if (!sessionID)
|
|
424
|
+
return;
|
|
425
|
+
try {
|
|
426
|
+
const tags = getTags(directory);
|
|
427
|
+
const memoriesResult = await memoryClient.searchMemoriesBySessionID(sessionID, tags.project.tag, CONFIG.compaction.memoryLimit);
|
|
428
|
+
if (!memoriesResult.success || memoriesResult.results.length === 0) {
|
|
429
|
+
return;
|
|
430
|
+
}
|
|
431
|
+
const memoryContext = formatMemoriesForCompaction(memoriesResult.results);
|
|
432
|
+
await ctx.client.session.prompt({
|
|
433
|
+
path: { id: sessionID },
|
|
434
|
+
body: {
|
|
435
|
+
parts: [{ id: `prt-compaction-${Date.now()}`, type: "text", text: memoryContext }],
|
|
436
|
+
noReply: true,
|
|
437
|
+
},
|
|
438
|
+
});
|
|
439
|
+
if (ctx.client?.tui) {
|
|
440
|
+
await ctx.client.tui
|
|
441
|
+
.showToast({
|
|
442
|
+
body: {
|
|
443
|
+
title: "Memory Restored",
|
|
444
|
+
message: `${memoriesResult.results.length} memories injected after compaction`,
|
|
445
|
+
variant: "success",
|
|
446
|
+
duration: 3000,
|
|
447
|
+
},
|
|
448
|
+
})
|
|
449
|
+
.catch(() => { });
|
|
450
|
+
}
|
|
451
|
+
log("Compaction memory injected", {
|
|
452
|
+
sessionID,
|
|
453
|
+
count: memoriesResult.results.length,
|
|
454
|
+
});
|
|
455
|
+
}
|
|
456
|
+
catch (error) {
|
|
457
|
+
log("Compaction handler error", { error: String(error) });
|
|
458
|
+
}
|
|
459
|
+
}
|
|
460
|
+
},
|
|
461
|
+
};
|
|
462
|
+
};
|
|
463
|
+
function formatSearchResults(query, results, limit) {
|
|
464
|
+
const memoryResults = results.results || [];
|
|
465
|
+
return JSON.stringify({
|
|
466
|
+
success: true,
|
|
467
|
+
query,
|
|
468
|
+
count: memoryResults.length,
|
|
469
|
+
results: memoryResults.slice(0, limit || 10).map((r) => ({
|
|
470
|
+
id: r.id,
|
|
471
|
+
content: r.memory || r.chunk,
|
|
472
|
+
similarity: Math.round(r.similarity * 100),
|
|
473
|
+
})),
|
|
474
|
+
});
|
|
475
|
+
}
|
|
476
|
+
function formatMemoriesForCompaction(memories) {
|
|
477
|
+
let output = `## Restored Session Memory\n\n`;
|
|
478
|
+
memories.forEach((m, i) => {
|
|
479
|
+
output += `### Memory ${i + 1}\n`;
|
|
480
|
+
output += `${m.memory}\n\n`;
|
|
481
|
+
if (m.tags && m.tags.length > 0) {
|
|
482
|
+
output += `Tags: ${m.tags.join(", ")}\n\n`;
|
|
483
|
+
}
|
|
484
|
+
});
|
|
485
|
+
return output;
|
|
486
|
+
}
|
package/dist/plugin.d.ts
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
declare const OpenCodeMemPlugin: import("@opencode-ai/plugin").Plugin;
|
|
2
|
+
export declare const id: string;
|
|
3
|
+
export { OpenCodeMemPlugin };
|
|
4
|
+
declare const _default: {
|
|
5
|
+
id: string;
|
|
6
|
+
server: import("@opencode-ai/plugin").Plugin;
|
|
7
|
+
};
|
|
8
|
+
export default _default;
|
|
9
|
+
//# sourceMappingURL=plugin.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"plugin.d.ts","sourceRoot":"","sources":["../src/plugin.ts"],"names":[],"mappings":"AAEA,QAAA,MAAQ,iBAAiB,sCAA+B,CAAC;AAEzD,eAAO,MAAM,EAAE,QACqE,CAAC;AACrF,OAAO,EAAE,iBAAiB,EAAE,CAAC;;;;;AAC7B,wBAAwE"}
|
package/dist/plugin.js
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import pkg from "../package.json" with { type: "json" };
|
|
2
|
+
const { OpenCodeMemPlugin } = await import("./index.js");
|
|
3
|
+
export const id = typeof pkg.name === "string" && pkg.name.trim() ? pkg.name.trim() : "opencode-mem";
|
|
4
|
+
export { OpenCodeMemPlugin };
|
|
5
|
+
export default { id, server: OpenCodeMemPlugin };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { BaseAIProvider, type ProviderConfig } from "./providers/base-provider.js";
|
|
2
|
+
import type { AIProviderType } from "./session/session-types.js";
|
|
3
|
+
export declare class AIProviderFactory {
|
|
4
|
+
static createProvider(providerType: AIProviderType, config: ProviderConfig): BaseAIProvider;
|
|
5
|
+
static getSupportedProviders(): AIProviderType[];
|
|
6
|
+
static cleanupExpiredSessions(): number;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=ai-provider-factory.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ai-provider-factory.d.ts","sourceRoot":"","sources":["../../../src/services/ai/ai-provider-factory.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAC;AAMnF,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAEjE,qBAAa,iBAAiB;IAC5B,MAAM,CAAC,cAAc,CAAC,YAAY,EAAE,cAAc,EAAE,MAAM,EAAE,cAAc,GAAG,cAAc;IAmB3F,MAAM,CAAC,qBAAqB,IAAI,cAAc,EAAE;IAIhD,MAAM,CAAC,sBAAsB,IAAI,MAAM;CAGxC"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { BaseAIProvider } from "./providers/base-provider.js";
|
|
2
|
+
import { OpenAIChatCompletionProvider } from "./providers/openai-chat-completion.js";
|
|
3
|
+
import { OpenAIResponsesProvider } from "./providers/openai-responses.js";
|
|
4
|
+
import { AnthropicMessagesProvider } from "./providers/anthropic-messages.js";
|
|
5
|
+
import { GoogleGeminiProvider } from "./providers/google-gemini.js";
|
|
6
|
+
import { aiSessionManager } from "./session/ai-session-manager.js";
|
|
7
|
+
export class AIProviderFactory {
|
|
8
|
+
static createProvider(providerType, config) {
|
|
9
|
+
switch (providerType) {
|
|
10
|
+
case "openai-chat":
|
|
11
|
+
return new OpenAIChatCompletionProvider(config, aiSessionManager);
|
|
12
|
+
case "openai-responses":
|
|
13
|
+
return new OpenAIResponsesProvider(config, aiSessionManager);
|
|
14
|
+
case "anthropic":
|
|
15
|
+
return new AnthropicMessagesProvider(config, aiSessionManager);
|
|
16
|
+
case "google-gemini":
|
|
17
|
+
return new GoogleGeminiProvider(config, aiSessionManager);
|
|
18
|
+
default:
|
|
19
|
+
throw new Error(`Unknown provider type: ${providerType}`);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
static getSupportedProviders() {
|
|
23
|
+
return ["openai-chat", "openai-responses", "anthropic", "google-gemini"];
|
|
24
|
+
}
|
|
25
|
+
static cleanupExpiredSessions() {
|
|
26
|
+
return aiSessionManager.cleanupExpiredSessions();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SDK-based structured output via opencode v2 session.prompt.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the old auth.json/OAuth-juggling flow. Instead of forging requests
|
|
5
|
+
* to provider HTTP endpoints ourselves, we delegate to the running opencode
|
|
6
|
+
* server: it already owns the user's auth (any provider, including
|
|
7
|
+
* github-copilot personal/business), token refresh, and provider routing.
|
|
8
|
+
*
|
|
9
|
+
* Per call we create a transient session, prompt with a JSON schema, then
|
|
10
|
+
* delete the session so it does not pollute the user's TUI session list.
|
|
11
|
+
*/
|
|
12
|
+
import type { z } from "zod";
|
|
13
|
+
import { type OpencodeClient } from "@opencode-ai/sdk/v2/client";
|
|
14
|
+
export declare function setConnectedProviders(providers: string[]): void;
|
|
15
|
+
export declare function isProviderConnected(providerName: string): boolean;
|
|
16
|
+
export declare function setV2Client(client: OpencodeClient): void;
|
|
17
|
+
export declare function getV2Client(): OpencodeClient | undefined;
|
|
18
|
+
export declare function createV2Client(serverUrl: URL | string): OpencodeClient;
|
|
19
|
+
export interface StructuredOutputOptions<T> {
|
|
20
|
+
client: OpencodeClient;
|
|
21
|
+
providerID: string;
|
|
22
|
+
modelID: string;
|
|
23
|
+
systemPrompt: string;
|
|
24
|
+
userPrompt: string;
|
|
25
|
+
schema: z.ZodType<T>;
|
|
26
|
+
directory?: string;
|
|
27
|
+
retryCount?: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Generate one structured-output completion via opencode's v2 API.
|
|
31
|
+
* Throws on: session.create failure, prompt failure, AssistantMessage.error
|
|
32
|
+
* (StructuredOutputError / ApiError / ...), missing `info.structured`,
|
|
33
|
+
* or final Zod validation failure.
|
|
34
|
+
*/
|
|
35
|
+
export declare function generateStructuredOutput<T>(opts: StructuredOutputOptions<T>): Promise<T>;
|
|
36
|
+
//# 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":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAC7B,OAAO,EAAwB,KAAK,cAAc,EAAE,MAAM,4BAA4B,CAAC;AAKvF,wBAAgB,qBAAqB,CAAC,SAAS,EAAE,MAAM,EAAE,GAAG,IAAI,CAE/D;AAED,wBAAgB,mBAAmB,CAAC,YAAY,EAAE,MAAM,GAAG,OAAO,CAEjE;AAED,wBAAgB,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI,CAExD;AAED,wBAAgB,WAAW,IAAI,cAAc,GAAG,SAAS,CAExD;AAED,wBAAgB,cAAc,CAAC,SAAS,EAAE,GAAG,GAAG,MAAM,GAAG,cAAc,CAGtE;AAED,MAAM,WAAW,uBAAuB,CAAC,CAAC;IACxC,MAAM,EAAE,cAAc,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACrB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAAC,CAAC,EAAE,IAAI,EAAE,uBAAuB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,CAAC,CAAC,CAgF9F"}
|