opencode-graphiti 0.1.0 → 0.1.2

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 (72) hide show
  1. package/README.md +44 -27
  2. package/esm/src/config.d.ts +3 -0
  3. package/esm/src/config.d.ts.map +1 -1
  4. package/esm/src/config.js +21 -35
  5. package/esm/src/handlers/chat.d.ts +21 -0
  6. package/esm/src/handlers/chat.d.ts.map +1 -0
  7. package/esm/src/handlers/chat.js +127 -0
  8. package/esm/src/handlers/compacting.d.ts +15 -0
  9. package/esm/src/handlers/compacting.d.ts.map +1 -0
  10. package/esm/src/handlers/compacting.js +29 -0
  11. package/esm/src/handlers/event.d.ts +18 -0
  12. package/esm/src/handlers/event.d.ts.map +1 -0
  13. package/esm/src/handlers/event.js +132 -0
  14. package/esm/src/index.d.ts +3 -1
  15. package/esm/src/index.d.ts.map +1 -1
  16. package/esm/src/index.js +28 -419
  17. package/esm/src/services/client.d.ts +33 -5
  18. package/esm/src/services/client.d.ts.map +1 -1
  19. package/esm/src/services/client.js +42 -20
  20. package/esm/src/services/compaction.d.ts +18 -69
  21. package/esm/src/services/compaction.d.ts.map +1 -1
  22. package/esm/src/services/compaction.js +86 -112
  23. package/esm/src/services/context-limit.d.ts +14 -0
  24. package/esm/src/services/context-limit.d.ts.map +1 -0
  25. package/esm/src/services/context-limit.js +41 -0
  26. package/esm/src/services/context.d.ts +5 -0
  27. package/esm/src/services/context.d.ts.map +1 -1
  28. package/esm/src/services/context.js +19 -16
  29. package/esm/src/session.d.ts +90 -0
  30. package/esm/src/session.d.ts.map +1 -0
  31. package/esm/src/session.js +304 -0
  32. package/esm/src/types/index.d.ts +28 -9
  33. package/esm/src/types/index.d.ts.map +1 -1
  34. package/esm/src/utils.d.ts +21 -0
  35. package/esm/src/utils.d.ts.map +1 -0
  36. package/esm/src/utils.js +34 -0
  37. package/package.json +3 -2
  38. package/script/src/config.d.ts +3 -0
  39. package/script/src/config.d.ts.map +1 -1
  40. package/script/src/config.js +21 -35
  41. package/script/src/handlers/chat.d.ts +21 -0
  42. package/script/src/handlers/chat.d.ts.map +1 -0
  43. package/script/src/handlers/chat.js +130 -0
  44. package/script/src/handlers/compacting.d.ts +15 -0
  45. package/script/src/handlers/compacting.d.ts.map +1 -0
  46. package/script/src/handlers/compacting.js +32 -0
  47. package/script/src/handlers/event.d.ts +18 -0
  48. package/script/src/handlers/event.d.ts.map +1 -0
  49. package/script/src/handlers/event.js +135 -0
  50. package/script/src/index.d.ts +3 -1
  51. package/script/src/index.d.ts.map +1 -1
  52. package/script/src/index.js +30 -422
  53. package/script/src/services/client.d.ts +33 -5
  54. package/script/src/services/client.d.ts.map +1 -1
  55. package/script/src/services/client.js +42 -53
  56. package/script/src/services/compaction.d.ts +18 -69
  57. package/script/src/services/compaction.d.ts.map +1 -1
  58. package/script/src/services/compaction.js +86 -113
  59. package/script/src/services/context-limit.d.ts +14 -0
  60. package/script/src/services/context-limit.d.ts.map +1 -0
  61. package/script/src/services/context-limit.js +45 -0
  62. package/script/src/services/context.d.ts +5 -0
  63. package/script/src/services/context.d.ts.map +1 -1
  64. package/script/src/services/context.js +22 -16
  65. package/script/src/session.d.ts +90 -0
  66. package/script/src/session.d.ts.map +1 -0
  67. package/script/src/session.js +308 -0
  68. package/script/src/types/index.d.ts +28 -9
  69. package/script/src/types/index.d.ts.map +1 -1
  70. package/script/src/utils.d.ts +21 -0
  71. package/script/src/utils.d.ts.map +1 -0
  72. package/script/src/utils.js +44 -0
package/esm/src/index.js CHANGED
@@ -1,22 +1,14 @@
1
1
  import { loadConfig } from "./config.js";
2
+ import { createChatHandler } from "./handlers/chat.js";
3
+ import { createCompactingHandler } from "./handlers/compacting.js";
4
+ import { createEventHandler } from "./handlers/event.js";
2
5
  import { GraphitiClient } from "./services/client.js";
3
- import { createPreemptiveCompactionHandler, getCompactionContext, handleCompaction, } from "./services/compaction.js";
4
- import { formatMemoryContext } from "./services/context.js";
5
6
  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();
7
+ import { SessionManager } from "./session.js";
8
+ import { makeGroupId, makeUserGroupId } from "./utils.js";
9
+ /**
10
+ * OpenCode plugin entry point for Graphiti memory integration.
11
+ */
20
12
  export const graphiti = async (input) => {
21
13
  const config = loadConfig();
22
14
  const client = new GraphitiClient(config.endpoint);
@@ -27,410 +19,27 @@ export const graphiti = async (input) => {
27
19
  logger.warn("Memory features will be unavailable until connection is established");
28
20
  }
29
21
  const defaultGroupId = makeGroupId(config.groupIdPrefix, input.directory);
22
+ const defaultUserGroupId = makeUserGroupId(config.groupIdPrefix);
30
23
  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
- });
24
+ const sessionManager = new SessionManager(defaultGroupId, defaultUserGroupId, sdkClient, client);
234
25
  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
- },
26
+ event: createEventHandler({
27
+ sessionManager,
28
+ client,
29
+ defaultGroupId,
30
+ sdkClient: sdkClient,
31
+ directory: input.directory,
32
+ groupIdPrefix: config.groupIdPrefix,
33
+ }),
34
+ "chat.message": createChatHandler({
35
+ sessionManager,
36
+ injectionInterval: config.injectionInterval,
37
+ client,
38
+ }),
39
+ "experimental.session.compacting": createCompactingHandler({
40
+ sessionManager,
41
+ client,
42
+ defaultGroupId,
43
+ }),
435
44
  };
436
45
  };
@@ -1,16 +1,39 @@
1
- import type { GraphitiEpisode, GraphitiFact, GraphitiNode } from "../types/index.js";
1
+ import type { GraphitiFact, GraphitiNode } from "../types/index.js";
2
+ /**
3
+ * Graphiti MCP client wrapper for connecting, querying,
4
+ * and persisting episodes with basic reconnection handling.
5
+ */
2
6
  export declare class GraphitiClient {
3
7
  private client;
4
8
  private transport;
5
9
  private connected;
6
10
  private endpoint;
11
+ /**
12
+ * Create a Graphiti client bound to the given MCP endpoint URL.
13
+ */
7
14
  constructor(endpoint: string);
15
+ /** Create a fresh MCP Client and Transport pair. */
16
+ private createClientAndTransport;
17
+ /**
18
+ * Establish a connection to the Graphiti MCP server.
19
+ * Creates a fresh Client/Transport if a previous attempt failed.
20
+ */
8
21
  connect(): Promise<boolean>;
22
+ /**
23
+ * Close the underlying MCP client connection.
24
+ */
9
25
  disconnect(): Promise<void>;
10
26
  private callTool;
11
27
  private isSessionExpired;
12
28
  private reconnect;
29
+ /**
30
+ * Parse MCP tool results into JSON when possible.
31
+ * Public for testing.
32
+ */
13
33
  parseToolResult(result: unknown): unknown;
34
+ /**
35
+ * Add an episode to Graphiti memory.
36
+ */
14
37
  addEpisode(params: {
15
38
  name: string;
16
39
  episodeBody: string;
@@ -18,20 +41,25 @@ export declare class GraphitiClient {
18
41
  source?: "text" | "json" | "message";
19
42
  sourceDescription?: string;
20
43
  }): Promise<void>;
44
+ /**
45
+ * Search Graphiti facts matching the provided query.
46
+ */
21
47
  searchFacts(params: {
22
48
  query: string;
23
49
  groupIds?: string[];
24
50
  maxFacts?: number;
25
51
  }): Promise<GraphitiFact[]>;
52
+ /**
53
+ * Search Graphiti nodes matching the provided query.
54
+ */
26
55
  searchNodes(params: {
27
56
  query: string;
28
57
  groupIds?: string[];
29
58
  maxNodes?: number;
30
59
  }): Promise<GraphitiNode[]>;
31
- getEpisodes(params: {
32
- groupIds?: string[];
33
- maxEpisodes?: number;
34
- }): Promise<GraphitiEpisode[]>;
60
+ /**
61
+ * Check whether the Graphiti MCP server is reachable.
62
+ */
35
63
  getStatus(): Promise<boolean>;
36
64
  }
37
65
  //# sourceMappingURL=client.d.ts.map
@@ -1 +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"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/src/services/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,YAAY,EAEZ,YAAY,EAEb,MAAM,mBAAmB,CAAC;AAG3B;;;GAGG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,MAAM,CAAS;IACvB,OAAO,CAAC,SAAS,CAAgC;IACjD,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,QAAQ,CAAS;IAEzB;;OAEG;gBACS,QAAQ,EAAE,MAAM;IAM5B,oDAAoD;IACpD,OAAO,CAAC,wBAAwB;IAOhC;;;OAGG;IACG,OAAO,IAAI,OAAO,CAAC,OAAO,CAAC;IAgBjC;;OAEG;IACG,UAAU,IAAI,OAAO,CAAC,IAAI,CAAC;YAOnB,QAAQ;IAkCtB,OAAO,CAAC,gBAAgB;YASV,SAAS;IAavB;;;OAGG;IACH,eAAe,CAAC,MAAM,EAAE,OAAO,GAAG,OAAO;IAyBzC;;OAEG;IACG,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;IAWjB;;OAEG;IACG,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;IAsB3B;;OAEG;IACG,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;IAsB3B;;OAEG;IACG,SAAS,IAAI,OAAO,CAAC,OAAO,CAAC;CAQpC"}
@@ -1,8 +1,14 @@
1
- import * as dntShim from "../../_dnt.shims.js";
2
1
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
3
2
  import { StreamableHTTPClientTransport } from "@modelcontextprotocol/sdk/client/streamableHttp.js";
4
3
  import { logger } from "./logger.js";
4
+ /**
5
+ * Graphiti MCP client wrapper for connecting, querying,
6
+ * and persisting episodes with basic reconnection handling.
7
+ */
5
8
  export class GraphitiClient {
9
+ /**
10
+ * Create a Graphiti client bound to the given MCP endpoint URL.
11
+ */
6
12
  constructor(endpoint) {
7
13
  Object.defineProperty(this, "client", {
8
14
  enumerable: true,
@@ -30,12 +36,23 @@ export class GraphitiClient {
30
36
  });
31
37
  this.endpoint = endpoint;
32
38
  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);
39
+ this.transport = new StreamableHTTPClientTransport(new URL(endpoint));
35
40
  }
41
+ /** Create a fresh MCP Client and Transport pair. */
42
+ createClientAndTransport() {
43
+ this.client = new Client({ name: "opencode-graphiti", version: "0.1.0" });
44
+ this.transport = new StreamableHTTPClientTransport(new URL(this.endpoint));
45
+ }
46
+ /**
47
+ * Establish a connection to the Graphiti MCP server.
48
+ * Creates a fresh Client/Transport if a previous attempt failed.
49
+ */
36
50
  async connect() {
37
51
  if (this.connected)
38
52
  return true;
53
+ // If a previous connect() tainted the Client's internal state,
54
+ // create fresh instances so the retry starts cleanly.
55
+ this.createClientAndTransport();
39
56
  try {
40
57
  await this.client.connect(this.transport);
41
58
  this.connected = true;
@@ -47,6 +64,9 @@ export class GraphitiClient {
47
64
  return false;
48
65
  }
49
66
  }
67
+ /**
68
+ * Close the underlying MCP client connection.
69
+ */
50
70
  async disconnect() {
51
71
  if (this.connected) {
52
72
  await this.client.close();
@@ -90,17 +110,20 @@ export class GraphitiClient {
90
110
  async reconnect() {
91
111
  this.connected = false;
92
112
  try {
93
- await this.transport.close();
113
+ await this.client.close();
94
114
  }
95
115
  catch {
96
- // ignore transport close errors
116
+ // ignore close errors on stale client
97
117
  }
98
- this.transport = new StreamableHTTPClientTransport(new URL(this.endpoint));
118
+ this.createClientAndTransport();
99
119
  await this.client.connect(this.transport);
100
120
  this.connected = true;
101
121
  logger.info("Reconnected to Graphiti MCP server");
102
122
  }
103
- // Public for testing
123
+ /**
124
+ * Parse MCP tool results into JSON when possible.
125
+ * Public for testing.
126
+ */
104
127
  parseToolResult(result) {
105
128
  const typedResult = result;
106
129
  const content = typedResult.content;
@@ -124,6 +147,9 @@ export class GraphitiClient {
124
147
  return text;
125
148
  }
126
149
  }
150
+ /**
151
+ * Add an episode to Graphiti memory.
152
+ */
127
153
  async addEpisode(params) {
128
154
  await this.callTool("add_memory", {
129
155
  name: params.name,
@@ -134,6 +160,9 @@ export class GraphitiClient {
134
160
  });
135
161
  logger.debug("Added episode:", params.name);
136
162
  }
163
+ /**
164
+ * Search Graphiti facts matching the provided query.
165
+ */
137
166
  async searchFacts(params) {
138
167
  try {
139
168
  const result = await this.callTool("search_memory_facts", {
@@ -155,6 +184,9 @@ export class GraphitiClient {
155
184
  return [];
156
185
  }
157
186
  }
187
+ /**
188
+ * Search Graphiti nodes matching the provided query.
189
+ */
158
190
  async searchNodes(params) {
159
191
  try {
160
192
  const result = await this.callTool("search_nodes", {
@@ -176,19 +208,9 @@ export class GraphitiClient {
176
208
  return [];
177
209
  }
178
210
  }
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
- }
211
+ /**
212
+ * Check whether the Graphiti MCP server is reachable.
213
+ */
192
214
  async getStatus() {
193
215
  try {
194
216
  await this.callTool("get_status", {});