opencode-graphiti 0.1.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.
Files changed (59) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +176 -0
  3. package/esm/_dnt.shims.d.ts +6 -0
  4. package/esm/_dnt.shims.d.ts.map +1 -0
  5. package/esm/_dnt.shims.js +61 -0
  6. package/esm/mod.d.ts +2 -0
  7. package/esm/mod.d.ts.map +1 -0
  8. package/esm/mod.js +1 -0
  9. package/esm/package.json +3 -0
  10. package/esm/src/config.d.ts +3 -0
  11. package/esm/src/config.d.ts.map +1 -0
  12. package/esm/src/config.js +43 -0
  13. package/esm/src/index.d.ts +4 -0
  14. package/esm/src/index.d.ts.map +1 -0
  15. package/esm/src/index.js +436 -0
  16. package/esm/src/services/client.d.ts +37 -0
  17. package/esm/src/services/client.d.ts.map +1 -0
  18. package/esm/src/services/client.js +201 -0
  19. package/esm/src/services/compaction.d.ts +93 -0
  20. package/esm/src/services/compaction.d.ts.map +1 -0
  21. package/esm/src/services/compaction.js +144 -0
  22. package/esm/src/services/context.d.ts +3 -0
  23. package/esm/src/services/context.d.ts.map +1 -0
  24. package/esm/src/services/context.js +36 -0
  25. package/esm/src/services/logger.d.ts +7 -0
  26. package/esm/src/services/logger.d.ts.map +1 -0
  27. package/esm/src/services/logger.js +22 -0
  28. package/esm/src/types/index.d.ts +48 -0
  29. package/esm/src/types/index.d.ts.map +1 -0
  30. package/esm/src/types/index.js +1 -0
  31. package/package.json +34 -0
  32. package/script/_dnt.shims.d.ts +6 -0
  33. package/script/_dnt.shims.d.ts.map +1 -0
  34. package/script/_dnt.shims.js +65 -0
  35. package/script/mod.d.ts +2 -0
  36. package/script/mod.d.ts.map +1 -0
  37. package/script/mod.js +17 -0
  38. package/script/package.json +3 -0
  39. package/script/src/config.d.ts +3 -0
  40. package/script/src/config.d.ts.map +1 -0
  41. package/script/src/config.js +79 -0
  42. package/script/src/index.d.ts +4 -0
  43. package/script/src/index.d.ts.map +1 -0
  44. package/script/src/index.js +441 -0
  45. package/script/src/services/client.d.ts +37 -0
  46. package/script/src/services/client.d.ts.map +1 -0
  47. package/script/src/services/client.js +238 -0
  48. package/script/src/services/compaction.d.ts +93 -0
  49. package/script/src/services/compaction.d.ts.map +1 -0
  50. package/script/src/services/compaction.js +149 -0
  51. package/script/src/services/context.d.ts +3 -0
  52. package/script/src/services/context.d.ts.map +1 -0
  53. package/script/src/services/context.js +39 -0
  54. package/script/src/services/logger.d.ts +7 -0
  55. package/script/src/services/logger.d.ts.map +1 -0
  56. package/script/src/services/logger.js +61 -0
  57. package/script/src/types/index.d.ts +48 -0
  58. package/script/src/types/index.d.ts.map +1 -0
  59. package/script/src/types/index.js +2 -0
@@ -0,0 +1,436 @@
1
+ import { loadConfig } from "./config.js";
2
+ import { GraphitiClient } from "./services/client.js";
3
+ import { createPreemptiveCompactionHandler, getCompactionContext, handleCompaction, } from "./services/compaction.js";
4
+ import { formatMemoryContext } from "./services/context.js";
5
+ import { logger } from "./services/logger.js";
6
+ export const makeGroupId = (prefix, directory) => {
7
+ const parts = directory?.split("/").filter(Boolean) ?? [];
8
+ const projectName = parts[parts.length - 1] || "default";
9
+ const rawGroupId = `${prefix}_${projectName}`;
10
+ return rawGroupId.replace(/[^A-Za-z0-9_-]/g, "_");
11
+ };
12
+ const isTextPart = (value) => {
13
+ if (!value || typeof value !== "object")
14
+ return false;
15
+ const part = value;
16
+ return part.type === "text" && typeof part.text === "string" &&
17
+ !part.synthetic;
18
+ };
19
+ const extractTextFromParts = (parts) => parts.filter(isTextPart).map((part) => part.text).join(" ").trim();
20
+ export const graphiti = async (input) => {
21
+ const config = loadConfig();
22
+ const client = new GraphitiClient(config.endpoint);
23
+ const sdkClient = input.client;
24
+ const connected = await client.connect();
25
+ if (!connected) {
26
+ logger.warn("Could not connect to Graphiti MCP server at", config.endpoint);
27
+ logger.warn("Memory features will be unavailable until connection is established");
28
+ }
29
+ const defaultGroupId = makeGroupId(config.groupIdPrefix, input.directory);
30
+ logger.info("Plugin initialized. Group ID:", defaultGroupId);
31
+ const sessions = new Map();
32
+ const parentIdCache = new Map();
33
+ const pendingAssistantMessages = new Map();
34
+ const bufferedAssistantMessageIds = new Set();
35
+ const resolveParentId = async (sessionId) => {
36
+ if (parentIdCache.has(sessionId)) {
37
+ return parentIdCache.get(sessionId) ?? null;
38
+ }
39
+ try {
40
+ const response = await sdkClient.session.get({
41
+ path: { id: sessionId },
42
+ });
43
+ const sessionInfo = typeof response === "object" && response !== null &&
44
+ "data" in response
45
+ ? response.data
46
+ : response;
47
+ if (!sessionInfo)
48
+ return undefined;
49
+ const parentId = sessionInfo.parentID ?? null;
50
+ parentIdCache.set(sessionId, parentId);
51
+ return parentId;
52
+ }
53
+ catch (err) {
54
+ logger.debug("Failed to resolve session parentID", { sessionId, err });
55
+ return undefined;
56
+ }
57
+ };
58
+ const resolveSessionState = async (sessionId) => {
59
+ const parentId = await resolveParentId(sessionId);
60
+ if (parentId === undefined)
61
+ return { state: null, resolved: false };
62
+ if (parentId) {
63
+ sessions.delete(sessionId);
64
+ return { state: null, resolved: true };
65
+ }
66
+ let state = sessions.get(sessionId);
67
+ if (!state) {
68
+ state = {
69
+ groupId: defaultGroupId,
70
+ injectedMemories: false,
71
+ messageCount: 0,
72
+ pendingMessages: [],
73
+ isMain: true,
74
+ };
75
+ sessions.set(sessionId, state);
76
+ }
77
+ return { state, resolved: true };
78
+ };
79
+ const isSubagentSession = async (sessionId) => {
80
+ const parentId = await resolveParentId(sessionId);
81
+ return !!parentId;
82
+ };
83
+ const fetchLatestAssistantMessage = async (sessionId) => {
84
+ try {
85
+ const response = await sdkClient.session.messages({
86
+ sessionID: sessionId,
87
+ limit: 20,
88
+ });
89
+ const payload = response && typeof response === "object" &&
90
+ "data" in response
91
+ ? response.data
92
+ : response;
93
+ const messages = Array.isArray(payload)
94
+ ? payload
95
+ : [];
96
+ if (messages.length === 0)
97
+ return null;
98
+ const lastAssistant = [...messages]
99
+ .reverse()
100
+ .find((message) => message.info?.role === "assistant");
101
+ if (!lastAssistant)
102
+ return null;
103
+ const text = extractTextFromParts(lastAssistant.parts);
104
+ if (!text)
105
+ return null;
106
+ return { id: lastAssistant.info?.id, text };
107
+ }
108
+ catch (err) {
109
+ logger.debug("Failed to list session messages for fallback", {
110
+ sessionId,
111
+ err,
112
+ });
113
+ return null;
114
+ }
115
+ };
116
+ const finalizeAssistantMessage = (state, sessionId, messageId, source) => {
117
+ const key = `${sessionId}:${messageId}`;
118
+ if (bufferedAssistantMessageIds.has(key))
119
+ return;
120
+ const buffered = pendingAssistantMessages.get(key);
121
+ pendingAssistantMessages.delete(key);
122
+ bufferedAssistantMessageIds.add(key);
123
+ const messageText = buffered?.text?.trim() ?? "";
124
+ const messagePreview = messageText.slice(0, 120);
125
+ logger.info("Assistant message completed", {
126
+ hook: source,
127
+ sessionId,
128
+ messageID: messageId,
129
+ source,
130
+ messageLength: messageText.length,
131
+ preview: messagePreview,
132
+ });
133
+ if (!messageText) {
134
+ logger.debug("Assistant message completed without buffered text", {
135
+ hook: source,
136
+ sessionId,
137
+ messageID: messageId,
138
+ source,
139
+ });
140
+ return;
141
+ }
142
+ state.pendingMessages.push(`Assistant: ${messageText}`);
143
+ logger.info("Buffered assistant reply", {
144
+ hook: source,
145
+ sessionId,
146
+ messageID: messageId,
147
+ source,
148
+ messageLength: messageText.length,
149
+ preview: messagePreview,
150
+ });
151
+ };
152
+ const flushPendingMessages = async (sessionId, sourceDescription, minBytes) => {
153
+ const state = sessions.get(sessionId);
154
+ if (!state || state.pendingMessages.length === 0)
155
+ return;
156
+ const lastMessage = state.pendingMessages.at(-1);
157
+ if (lastMessage) {
158
+ const separatorIndex = lastMessage.indexOf(":");
159
+ const role = separatorIndex === -1
160
+ ? lastMessage.trim().toLowerCase()
161
+ : lastMessage.slice(0, separatorIndex).trim().toLowerCase();
162
+ if (role === "user") {
163
+ const fallback = await fetchLatestAssistantMessage(sessionId);
164
+ if (fallback?.text) {
165
+ const fallbackKey = fallback.id
166
+ ? `${sessionId}:${fallback.id}`
167
+ : undefined;
168
+ const alreadyBuffered = fallbackKey
169
+ ? bufferedAssistantMessageIds.has(fallbackKey)
170
+ : state.pendingMessages.some((message) => message.startsWith("Assistant:") &&
171
+ message.includes(fallback.text));
172
+ if (!alreadyBuffered) {
173
+ state.pendingMessages.push(`Assistant: ${fallback.text}`);
174
+ if (fallbackKey) {
175
+ bufferedAssistantMessageIds.add(fallbackKey);
176
+ }
177
+ logger.info("Fallback assistant fetch used", {
178
+ sessionId,
179
+ messageID: fallback.id,
180
+ messageLength: fallback.text.length,
181
+ });
182
+ }
183
+ }
184
+ }
185
+ }
186
+ const combined = state.pendingMessages.join("\n\n");
187
+ if (combined.length < minBytes)
188
+ return;
189
+ const messagesToFlush = [...state.pendingMessages];
190
+ state.pendingMessages = [];
191
+ const messageLines = messagesToFlush.map((message) => {
192
+ const separatorIndex = message.indexOf(":");
193
+ const role = separatorIndex === -1
194
+ ? "Unknown"
195
+ : message.slice(0, separatorIndex).trim();
196
+ const text = separatorIndex === -1
197
+ ? message
198
+ : message.slice(separatorIndex + 1).trim();
199
+ return `${role}: ${text}`;
200
+ });
201
+ try {
202
+ const name = combined.slice(0, 80).replace(/\n/g, " ");
203
+ logger.info(`Flushing ${messagesToFlush.length} buffered message(s).`);
204
+ logger.info(`Buffered message contents:\n${messageLines.join("\n")}`, { sessionId });
205
+ await client.addEpisode({
206
+ name: `Buffered messages: ${name}`,
207
+ episodeBody: combined,
208
+ groupId: state.groupId,
209
+ source: "text",
210
+ sourceDescription,
211
+ });
212
+ logger.info("Flushed buffered messages to Graphiti");
213
+ }
214
+ catch (err) {
215
+ logger.error(`Failed to flush messages for ${sessionId}:`, err);
216
+ const currentState = sessions.get(sessionId);
217
+ if (currentState) {
218
+ currentState.pendingMessages = [
219
+ ...messagesToFlush,
220
+ ...currentState.pendingMessages,
221
+ ];
222
+ }
223
+ }
224
+ };
225
+ const preemptiveCompaction = createPreemptiveCompactionHandler({
226
+ compactionThreshold: config.compactionThreshold,
227
+ minTokensForCompaction: config.minTokensForCompaction,
228
+ compactionCooldownMs: config.compactionCooldownMs,
229
+ autoResumeAfterCompaction: config.autoResumeAfterCompaction,
230
+ }, {
231
+ sdkClient: sdkClient,
232
+ directory: input.directory,
233
+ });
234
+ return {
235
+ event: async ({ event }) => {
236
+ try {
237
+ if (event.type === "session.created") {
238
+ const info = event.properties.info;
239
+ const sessionId = info.id;
240
+ const parentId = info.parentID ?? null;
241
+ const isMain = !parentId;
242
+ parentIdCache.set(sessionId, parentId);
243
+ logger.info("Session created:", {
244
+ sessionId,
245
+ isMain,
246
+ parentID: info.parentID,
247
+ });
248
+ if (isMain) {
249
+ sessions.set(sessionId, {
250
+ groupId: defaultGroupId,
251
+ injectedMemories: false,
252
+ messageCount: 0,
253
+ pendingMessages: [],
254
+ isMain,
255
+ });
256
+ }
257
+ else {
258
+ logger.debug("Ignoring subagent session:", sessionId);
259
+ }
260
+ return;
261
+ }
262
+ if (event.type === "session.compacted") {
263
+ const sessionId = event.properties.sessionID;
264
+ const { state, resolved } = await resolveSessionState(sessionId);
265
+ if (!resolved) {
266
+ logger.debug("Unable to resolve session compaction:", sessionId);
267
+ return;
268
+ }
269
+ if (!state?.isMain) {
270
+ logger.debug("Ignoring non-main compaction:", sessionId);
271
+ return;
272
+ }
273
+ const summary = event.properties.summary ||
274
+ "";
275
+ await flushPendingMessages(sessionId, "Buffered messages flushed before compaction", 0);
276
+ if (summary) {
277
+ await handleCompaction({
278
+ client,
279
+ config,
280
+ groupId: state.groupId,
281
+ summary,
282
+ sessionId,
283
+ });
284
+ }
285
+ return;
286
+ }
287
+ if (event.type === "session.idle") {
288
+ const sessionId = event.properties.sessionID;
289
+ const { state, resolved } = await resolveSessionState(sessionId);
290
+ if (!resolved) {
291
+ logger.debug("Unable to resolve idle session:", sessionId);
292
+ return;
293
+ }
294
+ if (!state?.isMain) {
295
+ logger.debug("Ignoring non-main idle session:", sessionId);
296
+ return;
297
+ }
298
+ await flushPendingMessages(sessionId, "Buffered messages from OpenCode session", 50);
299
+ return;
300
+ }
301
+ if (event.type === "message.updated") {
302
+ const info = event.properties.info;
303
+ const sessionId = info.sessionID;
304
+ logger.info("Message event fired", {
305
+ hook: "message.updated",
306
+ eventType: "message.updated",
307
+ sessionId,
308
+ role: info.role,
309
+ messageID: info.id,
310
+ });
311
+ const { state, resolved } = await resolveSessionState(sessionId);
312
+ if (!resolved) {
313
+ logger.debug("Unable to resolve session for message update:", {
314
+ sessionId,
315
+ messageID: info.id,
316
+ role: info.role,
317
+ });
318
+ return;
319
+ }
320
+ if (!state?.isMain) {
321
+ logger.debug("Ignoring non-main message update:", sessionId);
322
+ return;
323
+ }
324
+ if (info.role !== "assistant") {
325
+ pendingAssistantMessages.delete(`${sessionId}:${info.id}`);
326
+ return;
327
+ }
328
+ const key = `${sessionId}:${info.id}`;
329
+ const time = info.time;
330
+ if (!time?.completed)
331
+ return;
332
+ if (bufferedAssistantMessageIds.has(key))
333
+ return;
334
+ finalizeAssistantMessage(state, sessionId, info.id, "message.updated");
335
+ if (info.tokens && info.providerID && info.modelID) {
336
+ preemptiveCompaction
337
+ .checkAndTriggerCompaction(sessionId, info.tokens, info.providerID, info.modelID)
338
+ .catch((err) => logger.error("Preemptive compaction check failed", err));
339
+ }
340
+ return;
341
+ }
342
+ if (event.type === "message.part.updated") {
343
+ const part = event.properties.part;
344
+ if (part.type !== "text" || part.synthetic)
345
+ return;
346
+ const sessionId = part.sessionID;
347
+ const messageId = part.messageID;
348
+ const key = `${sessionId}:${messageId}`;
349
+ pendingAssistantMessages.set(key, {
350
+ sessionId,
351
+ text: part.text,
352
+ });
353
+ }
354
+ }
355
+ catch (err) {
356
+ logger.error("Event handler error", { type: event.type, err });
357
+ }
358
+ },
359
+ "chat.message": async ({ sessionID }, output) => {
360
+ if (await isSubagentSession(sessionID)) {
361
+ logger.debug("Ignoring subagent chat message:", sessionID);
362
+ return;
363
+ }
364
+ const { state, resolved } = await resolveSessionState(sessionID);
365
+ if (!resolved) {
366
+ output.allow_buffering = true;
367
+ logger.debug("Unable to resolve session for message:", { sessionID });
368
+ return;
369
+ }
370
+ if (!state?.isMain) {
371
+ logger.debug("Ignoring subagent chat message:", sessionID);
372
+ return;
373
+ }
374
+ state.messageCount++;
375
+ const messageText = extractTextFromParts(output.parts);
376
+ if (!messageText)
377
+ return;
378
+ state.pendingMessages.push(`User: ${messageText}`);
379
+ logger.info("Buffered user message", {
380
+ hook: "chat.message",
381
+ sessionID,
382
+ messageLength: messageText.length,
383
+ });
384
+ if (!state.injectedMemories && config.injectOnFirstMessage) {
385
+ state.injectedMemories = true;
386
+ try {
387
+ const [facts, nodes] = await Promise.all([
388
+ client.searchFacts({
389
+ query: messageText,
390
+ groupIds: [state.groupId],
391
+ maxFacts: config.maxFacts,
392
+ }),
393
+ client.searchNodes({
394
+ query: messageText,
395
+ groupIds: [state.groupId],
396
+ maxNodes: config.maxNodes,
397
+ }),
398
+ ]);
399
+ const memoryContext = formatMemoryContext(facts, nodes);
400
+ if (memoryContext) {
401
+ output.parts.unshift({
402
+ type: "text",
403
+ text: memoryContext,
404
+ id: `graphiti-memory-${Date.now()}`,
405
+ sessionID: output.message.sessionID,
406
+ messageID: output.message.id,
407
+ synthetic: true,
408
+ });
409
+ logger.info(`Injected ${facts.length} facts and ${nodes.length} nodes`);
410
+ }
411
+ }
412
+ catch (err) {
413
+ logger.error("Failed to inject memories:", err);
414
+ }
415
+ }
416
+ },
417
+ "experimental.session.compacting": async ({ sessionID }, output) => {
418
+ const state = sessions.get(sessionID);
419
+ if (!state?.isMain) {
420
+ logger.debug("Ignoring non-main compaction context:", sessionID);
421
+ return;
422
+ }
423
+ const groupId = state.groupId || defaultGroupId;
424
+ const additionalContext = await getCompactionContext({
425
+ client,
426
+ config,
427
+ groupId,
428
+ contextStrings: output.context,
429
+ });
430
+ if (additionalContext.length > 0) {
431
+ output.context.push(...additionalContext);
432
+ logger.info("Injected persistent knowledge into compaction context");
433
+ }
434
+ },
435
+ };
436
+ };
@@ -0,0 +1,37 @@
1
+ import type { GraphitiEpisode, GraphitiFact, GraphitiNode } from "../types/index.js";
2
+ export declare class GraphitiClient {
3
+ private client;
4
+ private transport;
5
+ private connected;
6
+ private endpoint;
7
+ constructor(endpoint: string);
8
+ connect(): Promise<boolean>;
9
+ disconnect(): Promise<void>;
10
+ private callTool;
11
+ private isSessionExpired;
12
+ private reconnect;
13
+ parseToolResult(result: unknown): unknown;
14
+ addEpisode(params: {
15
+ name: string;
16
+ episodeBody: string;
17
+ groupId?: string;
18
+ source?: "text" | "json" | "message";
19
+ sourceDescription?: string;
20
+ }): Promise<void>;
21
+ searchFacts(params: {
22
+ query: string;
23
+ groupIds?: string[];
24
+ maxFacts?: number;
25
+ }): Promise<GraphitiFact[]>;
26
+ searchNodes(params: {
27
+ query: string;
28
+ groupIds?: string[];
29
+ maxNodes?: number;
30
+ }): Promise<GraphitiNode[]>;
31
+ getEpisodes(params: {
32
+ groupIds?: string[];
33
+ maxEpisodes?: number;
34
+ }): Promise<GraphitiEpisode[]>;
35
+ getStatus(): Promise<boolean>;
36
+ }
37
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/src/services/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,eAAe,EACf,YAAY,EAEZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAG3B,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAS;gBAEb,QAAQ,EAAE,MAAM;IAStB,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAa3B,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAOnB,QAAQ;IAkCtB,OAAO,CAAC,gBAAgB;YASV,SAAS;IAgBvB,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO;IAyBnC,UAAU,CAAC,MAAM,EAAE;QACvB,IAAI,EAAE,MAAM,CAAC;QACb,WAAW,EAAE,MAAM,CAAC;QACpB,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAAC;QACrC,iBAAiB,CAAC,EAAE,MAAM,CAAC;KAC5B,GAAG,OAAO,CAAC,IAAI,CAAC;IAWX,WAAW,CAAC,MAAM,EAAE;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAsBrB,WAAW,CAAC,MAAM,EAAE;QACxB,KAAK,EAAE,MAAM,CAAC;QACd,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAsBrB,WAAW,CAAC,MAAM,EAAE;QACxB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;QACpB,WAAW,CAAC,EAAE,MAAM,CAAC;KACtB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IAaxB,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAQpC"}
@@ -0,0 +1,201 @@
1
+ import * as dntShim from "../../_dnt.shims.js";
2
+ import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
+ import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
+ import { logger } from "./logger.js";
5
+ export class GraphitiClient {
6
+ constructor(endpoint) {
7
+ Object.defineProperty(this, "client", {
8
+ enumerable: true,
9
+ configurable: true,
10
+ writable: true,
11
+ value: void 0
12
+ });
13
+ Object.defineProperty(this, "transport", {
14
+ enumerable: true,
15
+ configurable: true,
16
+ writable: true,
17
+ value: void 0
18
+ });
19
+ Object.defineProperty(this, "connected", {
20
+ enumerable: true,
21
+ configurable: true,
22
+ writable: true,
23
+ value: false
24
+ });
25
+ Object.defineProperty(this, "endpoint", {
26
+ enumerable: true,
27
+ configurable: true,
28
+ writable: true,
29
+ value: void 0
30
+ });
31
+ this.endpoint = endpoint;
32
+ this.client = new Client({ name: "opencode-graphiti", version: "0.1.0" });
33
+ const url = new dntShim.dntGlobalThis.URL(endpoint);
34
+ this.transport = new StreamableHTTPClientTransport(url);
35
+ }
36
+ async connect() {
37
+ if (this.connected)
38
+ return true;
39
+ try {
40
+ await this.client.connect(this.transport);
41
+ this.connected = true;
42
+ logger.info("Connected to Graphiti MCP server at", this.endpoint);
43
+ return true;
44
+ }
45
+ catch (err) {
46
+ logger.error("Failed to connect to Graphiti:", err);
47
+ return false;
48
+ }
49
+ }
50
+ async disconnect() {
51
+ if (this.connected) {
52
+ await this.client.close();
53
+ this.connected = false;
54
+ }
55
+ }
56
+ async callTool(name, args) {
57
+ if (!this.connected) {
58
+ const ok = await this.connect();
59
+ if (!ok)
60
+ throw new Error("Not connected to Graphiti");
61
+ }
62
+ // Sanitize arguments: omit task_id (and others) if null or undefined
63
+ const sanitizedArgs = Object.fromEntries(Object.entries(args).filter(([_, v]) => v !== null && v !== undefined));
64
+ try {
65
+ const result = await this.client.callTool({
66
+ name,
67
+ arguments: sanitizedArgs,
68
+ });
69
+ return this.parseToolResult(result);
70
+ }
71
+ catch (err) {
72
+ if (this.isSessionExpired(err)) {
73
+ logger.warn("Graphiti session expired, reconnecting...");
74
+ await this.reconnect();
75
+ const result = await this.client.callTool({
76
+ name,
77
+ arguments: sanitizedArgs,
78
+ });
79
+ return this.parseToolResult(result);
80
+ }
81
+ throw err;
82
+ }
83
+ }
84
+ isSessionExpired(err) {
85
+ return !!(err &&
86
+ typeof err === "object" &&
87
+ "code" in err &&
88
+ err.code === 404);
89
+ }
90
+ async reconnect() {
91
+ this.connected = false;
92
+ try {
93
+ await this.transport.close();
94
+ }
95
+ catch {
96
+ // ignore transport close errors
97
+ }
98
+ this.transport = new StreamableHTTPClientTransport(new URL(this.endpoint));
99
+ await this.client.connect(this.transport);
100
+ this.connected = true;
101
+ logger.info("Reconnected to Graphiti MCP server");
102
+ }
103
+ // Public for testing
104
+ parseToolResult(result) {
105
+ const typedResult = result;
106
+ const content = typedResult.content;
107
+ if (!Array.isArray(content) || content.length === 0)
108
+ return result;
109
+ const text = content.find((item) => item?.type === "text")?.text;
110
+ if (text === undefined)
111
+ return result;
112
+ if (typeof text !== "string") {
113
+ try {
114
+ return JSON.parse(String(text));
115
+ }
116
+ catch {
117
+ return text;
118
+ }
119
+ }
120
+ try {
121
+ return JSON.parse(text);
122
+ }
123
+ catch {
124
+ return text;
125
+ }
126
+ }
127
+ async addEpisode(params) {
128
+ await this.callTool("add_memory", {
129
+ name: params.name,
130
+ episode_body: params.episodeBody,
131
+ group_id: params.groupId,
132
+ source: params.source || "text",
133
+ source_description: params.sourceDescription || "",
134
+ });
135
+ logger.debug("Added episode:", params.name);
136
+ }
137
+ async searchFacts(params) {
138
+ try {
139
+ const result = await this.callTool("search_memory_facts", {
140
+ query: params.query,
141
+ group_ids: params.groupIds,
142
+ max_facts: params.maxFacts || 10,
143
+ });
144
+ if (Array.isArray(result))
145
+ return result;
146
+ if (result &&
147
+ typeof result === "object" &&
148
+ Array.isArray(result.facts)) {
149
+ return result.facts;
150
+ }
151
+ return [];
152
+ }
153
+ catch (err) {
154
+ logger.error("searchFacts error:", err);
155
+ return [];
156
+ }
157
+ }
158
+ async searchNodes(params) {
159
+ try {
160
+ const result = await this.callTool("search_nodes", {
161
+ query: params.query,
162
+ group_ids: params.groupIds,
163
+ max_nodes: params.maxNodes || 10,
164
+ });
165
+ if (Array.isArray(result))
166
+ return result;
167
+ if (result &&
168
+ typeof result === "object" &&
169
+ Array.isArray(result.nodes)) {
170
+ return result.nodes;
171
+ }
172
+ return [];
173
+ }
174
+ catch (err) {
175
+ logger.error("searchNodes error:", err);
176
+ return [];
177
+ }
178
+ }
179
+ async getEpisodes(params) {
180
+ try {
181
+ const result = await this.callTool("get_episodes", {
182
+ group_ids: params.groupIds,
183
+ max_episodes: params.maxEpisodes || 10,
184
+ });
185
+ return result || [];
186
+ }
187
+ catch (err) {
188
+ logger.error("getEpisodes error:", err);
189
+ return [];
190
+ }
191
+ }
192
+ async getStatus() {
193
+ try {
194
+ await this.callTool("get_status", {});
195
+ return true;
196
+ }
197
+ catch {
198
+ return false;
199
+ }
200
+ }
201
+ }