@rlabs-inc/memory 0.5.3 → 0.5.9

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.
@@ -15,33 +15,7 @@ import type {
15
15
  } from "../types/memory.ts";
16
16
  import { logger } from "../utils/logger.ts";
17
17
  import { parseSessionFile, type ParsedMessage } from "./session-parser.ts";
18
-
19
- /**
20
- * Get the correct Claude CLI command path
21
- * Uses `which` for universal discovery across installation methods
22
- */
23
- function getClaudeCommand(): string {
24
- // 1. Check for explicit override
25
- const envCommand = process.env.CURATOR_COMMAND;
26
- if (envCommand) {
27
- return envCommand;
28
- }
29
-
30
- // 2. Use `which` to find claude in PATH (universal - works with native, homebrew, npm, etc.)
31
- const result = Bun.spawnSync(["which", "claude"]);
32
- if (result.exitCode === 0) {
33
- return result.stdout.toString().trim();
34
- }
35
-
36
- // 3. Legacy fallback - hardcoded native install path
37
- const claudeLocal = join(homedir(), ".claude", "local", "claude");
38
- if (existsSync(claudeLocal)) {
39
- return claudeLocal;
40
- }
41
-
42
- // 4. Last resort - assume it's in PATH
43
- return "claude";
44
- }
18
+ import { getClaudeCommand, getCuratorPromptPath } from "../utils/paths.ts";
45
19
 
46
20
  /**
47
21
  * Curator configuration
@@ -657,13 +631,13 @@ This session has ended. Please curate the memories from this conversation accord
657
631
  `Curator SDK raw response (${resultText.length} chars):`,
658
632
  "curator",
659
633
  );
660
- if (logger.isVerbose()) {
661
- const preview =
662
- resultText.length > 3000
663
- ? resultText.slice(0, 3000) + "...[truncated]"
664
- : resultText;
665
- console.log(preview);
666
- }
634
+ // if (logger.isVerbose()) {
635
+ // const preview =
636
+ // resultText.length > 3000
637
+ // ? resultText.slice(0, 3000) + "...[truncated]"
638
+ // : resultText;
639
+ // console.log(preview);
640
+ // }
667
641
 
668
642
  return this.parseCurationResponse(resultText);
669
643
  }
@@ -730,16 +704,17 @@ This session has ended. Please curate the memories from this conversation accord
730
704
  const curationPrompt = this.buildCurationPrompt(triggerType);
731
705
 
732
706
  logger.debug(
733
- `Curator v2: Resuming session ${claudeSessionId}`,
707
+ `Curator Claude - Resuming session ${claudeSessionId}`,
734
708
  "curator",
735
709
  );
736
710
 
737
711
  try {
738
712
  const q = query({
739
- prompt: "Curate memories from this session according to your system instructions. Return ONLY the JSON structure.",
713
+ prompt:
714
+ "Curate memories from this session according to your system instructions. Return ONLY the JSON structure.",
740
715
  options: {
741
716
  resume: claudeSessionId,
742
- appendSystemPrompt: curationPrompt, // APPEND, don't replace!
717
+ systemPrompt: curationPrompt,
743
718
  model: "claude-opus-4-5-20251101",
744
719
  permissionMode: "bypassPermissions",
745
720
  },
@@ -749,18 +724,22 @@ This session has ended. Please curate the memories from this conversation accord
749
724
  let resultText = "";
750
725
  for await (const message of q) {
751
726
  // Track usage for debugging
752
- if (message.type === "assistant" && "usage" in message && message.usage) {
727
+ if (
728
+ message.type === "assistant" &&
729
+ "usage" in message &&
730
+ message.usage
731
+ ) {
753
732
  logger.debug(
754
- `Curator v2: Tokens used - input: ${message.usage.input_tokens}, output: ${message.usage.output_tokens}`,
733
+ `Curator Claude - Tokens used - input: ${message.usage}, output: ${message.usage}`,
755
734
  "curator",
756
735
  );
757
736
  }
758
737
 
759
738
  // Get the result text
760
739
  if (message.type === "result") {
761
- if (message.subtype === "error") {
740
+ if (message.subtype !== "success") {
762
741
  logger.debug(
763
- `Curator v2: Error result - ${JSON.stringify(message)}`,
742
+ `Curator Claude - Error result - ${JSON.stringify(message)}`,
764
743
  "curator",
765
744
  );
766
745
  return { session_summary: "", memories: [] };
@@ -771,29 +750,28 @@ This session has ended. Please curate the memories from this conversation accord
771
750
  }
772
751
 
773
752
  if (!resultText) {
774
- logger.debug("Curator v2: No result text received", "curator");
753
+ logger.debug("Curator Claude - No result text received", "curator");
775
754
  return { session_summary: "", memories: [] };
776
755
  }
777
756
 
778
757
  // Log complete response for debugging
779
- logger.debug(
780
- `Curator v2: Complete response:\n${resultText}`,
781
- "curator",
782
- );
758
+ // logger.debug(
759
+ // `Curator Claude - Complete response:\n${resultText}`,
760
+ // "curator",
761
+ // );
783
762
 
784
763
  // Use existing battle-tested parser
785
764
  const result = this.parseCurationResponse(resultText);
786
765
 
787
766
  logger.debug(
788
- `Curator v2: Parsed ${result.memories.length} memories`,
767
+ `Curator Claude - Parsed ${result.memories.length} memories`,
789
768
  "curator",
790
769
  );
791
770
 
792
771
  return result;
793
-
794
772
  } catch (error: any) {
795
773
  logger.debug(
796
- `Curator v2: Session resume failed: ${error.message}`,
774
+ `Curator Claude - Session resume failed: ${error.message}`,
797
775
  "curator",
798
776
  );
799
777
  // Return empty - caller should fall back to transcript-based curation
@@ -809,38 +787,46 @@ This session has ended. Please curate the memories from this conversation accord
809
787
  async curateWithGeminiCLI(
810
788
  sessionId: string,
811
789
  triggerType: CurationTrigger = "session_end",
790
+ cwd?: string,
791
+ apiKey?: string,
812
792
  ): Promise<CurationResult> {
813
793
  const systemPrompt = this.buildCurationPrompt(triggerType);
814
794
  const userMessage =
815
795
  "This session has ended. Please curate the memories from our conversation according to the instructions in your system prompt. Return ONLY the JSON structure.";
816
796
 
817
- // Write system prompt to temp file
818
- const tempPromptPath = join(homedir(), ".local", "share", "memory", ".gemini-curator-prompt.md");
819
-
820
- // Ensure directory exists
821
- const tempDir = join(homedir(), ".local", "share", "memory");
822
- if (!existsSync(tempDir)) {
823
- const { mkdirSync } = await import("fs");
824
- mkdirSync(tempDir, { recursive: true });
825
- }
826
-
797
+ // Write system prompt to temp file (tmpdir always exists)
798
+ const tempPromptPath = getCuratorPromptPath();
827
799
  await Bun.write(tempPromptPath, systemPrompt);
828
800
 
829
801
  // Build CLI command
802
+ // Use --resume latest since SessionEnd hook fires immediately after session ends
830
803
  const args = [
831
- "--resume", sessionId,
832
- "-p", userMessage,
833
- "--output-format", "json",
804
+ "--resume",
805
+ "latest",
806
+ "-p",
807
+ userMessage,
808
+ "--output-format",
809
+ "json",
834
810
  ];
835
811
 
836
- logger.debug(`Curator Gemini: Spawning gemini CLI with session ${sessionId}`, "curator");
812
+ logger.debug(
813
+ `Curator Gemini - Spawning gemini CLI to resume latest session (triggered by ${sessionId})`,
814
+ "curator",
815
+ );
816
+ logger.debug(
817
+ `Curator Gemini - cwd = '${cwd}' | (type: ${typeof cwd})`,
818
+ "curator",
819
+ );
837
820
 
838
821
  // Execute CLI with system prompt via environment variable
822
+ // Must run from original project directory so --resume latest finds correct session
839
823
  const proc = Bun.spawn(["gemini", ...args], {
824
+ cwd: cwd || undefined,
840
825
  env: {
841
826
  ...process.env,
842
827
  MEMORY_CURATOR_ACTIVE: "1", // Prevent recursive hook triggering
843
828
  GEMINI_SYSTEM_MD: tempPromptPath, // Inject our curation prompt
829
+ ...(apiKey ? { GEMINI_API_KEY: apiKey } : {}),
844
830
  },
845
831
  stdout: "pipe",
846
832
  stderr: "pipe",
@@ -855,11 +841,14 @@ This session has ended. Please curate the memories from this conversation accord
855
841
 
856
842
  logger.debug(`Curator Gemini: Exit code ${exitCode}`, "curator");
857
843
  if (stderr && stderr.trim()) {
858
- logger.debug(`Curator Gemini stderr: ${stderr}`, "curator");
844
+ logger.debug(`Curator Gemini - stderr: ${stderr}`, "curator");
859
845
  }
860
846
 
861
847
  if (exitCode !== 0) {
862
- logger.debug(`Curator Gemini: Failed with exit code ${exitCode}`, "curator");
848
+ logger.debug(
849
+ `Curator Gemini - Failed with exit code ${exitCode}`,
850
+ "curator",
851
+ );
863
852
  return { session_summary: "", memories: [] };
864
853
  }
865
854
 
@@ -868,10 +857,16 @@ This session has ended. Please curate the memories from this conversation accord
868
857
  // We need to extract just the JSON object
869
858
  try {
870
859
  // Find the JSON object - it starts with { and we need to find the matching }
871
- const jsonStart = stdout.indexOf('{');
860
+ const jsonStart = stdout.indexOf("{");
872
861
  if (jsonStart === -1) {
873
- logger.debug("Curator Gemini: No JSON object found in output", "curator");
874
- logger.debug(`Curator Gemini: Raw stdout: ${stdout.slice(0, 500)}`, "curator");
862
+ logger.debug(
863
+ "Curator Gemini - No JSON object found in output",
864
+ "curator",
865
+ );
866
+ // logger.debug(
867
+ // `Curator Gemini - Raw stdout: ${stdout.slice(0, 500)}`,
868
+ // "curator",
869
+ // );
875
870
  return { session_summary: "", memories: [] };
876
871
  }
877
872
 
@@ -879,8 +874,8 @@ This session has ended. Please curate the memories from this conversation accord
879
874
  let braceCount = 0;
880
875
  let jsonEnd = -1;
881
876
  for (let i = jsonStart; i < stdout.length; i++) {
882
- if (stdout[i] === '{') braceCount++;
883
- if (stdout[i] === '}') braceCount--;
877
+ if (stdout[i] === "{") braceCount++;
878
+ if (stdout[i] === "}") braceCount--;
884
879
  if (braceCount === 0) {
885
880
  jsonEnd = i + 1;
886
881
  break;
@@ -888,20 +883,35 @@ This session has ended. Please curate the memories from this conversation accord
888
883
  }
889
884
 
890
885
  if (jsonEnd === -1) {
891
- logger.debug("Curator Gemini: Could not find matching closing brace", "curator");
886
+ logger.debug(
887
+ "Curator Gemini - Could not find matching closing brace",
888
+ "curator",
889
+ );
892
890
  return { session_summary: "", memories: [] };
893
891
  }
894
892
 
895
893
  const jsonStr = stdout.slice(jsonStart, jsonEnd);
896
- logger.debug(`Curator Gemini: Extracted JSON (${jsonStr.length} chars) from position ${jsonStart} to ${jsonEnd}`, "curator");
894
+ logger.debug(
895
+ `Curator Gemini - Extracted JSON (${jsonStr.length} chars) from position ${jsonStart} to ${jsonEnd}`,
896
+ "curator",
897
+ );
897
898
 
898
899
  let geminiOutput;
899
900
  try {
900
901
  geminiOutput = JSON.parse(jsonStr);
901
- logger.debug(`Curator Gemini: Parsed outer JSON successfully`, "curator");
902
+ logger.debug(
903
+ `Curator Gemini - Parsed outer JSON successfully`,
904
+ "curator",
905
+ );
902
906
  } catch (outerError: any) {
903
- logger.debug(`Curator Gemini: Outer JSON parse failed: ${outerError.message}`, "curator");
904
- logger.debug(`Curator Gemini: JSON string (first 500): ${jsonStr.slice(0, 500)}`, "curator");
907
+ logger.debug(
908
+ `Curator Gemini - Outer JSON parse failed: ${outerError.message}`,
909
+ "curator",
910
+ );
911
+ // logger.debug(
912
+ // `Curator Gemini - JSON string (first 500): ${jsonStr.slice(0, 500)}`,
913
+ // "curator",
914
+ // );
905
915
  return { session_summary: "", memories: [] };
906
916
  }
907
917
 
@@ -910,80 +920,52 @@ This session has ended. Please curate the memories from this conversation accord
910
920
  const aiResponse = geminiOutput.response || "";
911
921
 
912
922
  if (!aiResponse) {
913
- logger.debug("Curator Gemini: No response field in output", "curator");
923
+ logger.debug("Curator Gemini - No response field in output", "curator");
914
924
  return { session_summary: "", memories: [] };
915
925
  }
916
926
 
917
- logger.debug(`Curator Gemini: Got response (${aiResponse.length} chars)`, "curator");
927
+ logger.debug(
928
+ `Curator Gemini - Got response (${aiResponse.length} chars)`,
929
+ "curator",
930
+ );
918
931
 
919
932
  // Remove markdown code blocks if present
920
933
  let cleanResponse = aiResponse;
921
934
  const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
922
935
  if (codeBlockMatch) {
923
936
  cleanResponse = codeBlockMatch[1].trim();
924
- logger.debug(`Curator Gemini: Extracted JSON from code block (${cleanResponse.length} chars)`, "curator");
937
+ logger.debug(
938
+ `Curator Gemini - Extracted JSON from code block (${cleanResponse.length} chars)`,
939
+ "curator",
940
+ );
925
941
  } else {
926
- logger.debug(`Curator Gemini: No code block found, using raw response`, "curator");
942
+ logger.debug(
943
+ `Curator Gemini - No code block found, using raw response`,
944
+ "curator",
945
+ );
927
946
  }
928
947
 
929
- logger.debug(`Curator Gemini: Calling parseCurationResponse...`, "curator");
948
+ logger.debug(
949
+ `Curator Gemini - Calling parseCurationResponse...`,
950
+ "curator",
951
+ );
930
952
  // Use existing parser
931
953
  const result = this.parseCurationResponse(cleanResponse);
932
- logger.debug(`Curator Gemini: Parsed ${result.memories.length} memories`, "curator");
954
+ logger.debug(
955
+ `Curator Gemini - Parsed ${result.memories.length} memories`,
956
+ "curator",
957
+ );
933
958
  return result;
934
959
  } catch (error: any) {
935
960
  logger.debug(`Curator Gemini: Parse error: ${error.message}`, "curator");
936
- logger.debug(`Curator Gemini: Raw stdout (first 500 chars): ${stdout.slice(0, 500)}`, "curator");
961
+ // logger.debug(
962
+ // `Curator Gemini - Raw stdout (first 500 chars): ${stdout.slice(0, 500)}`,
963
+ // "curator",
964
+ // );
937
965
  return { session_summary: "", memories: [] };
938
966
  }
939
967
  }
940
968
 
941
- /**
942
- * Legacy method: Curate using Anthropic SDK with API key
943
- * Kept for backwards compatibility
944
- * @deprecated Use curateWithSDK() which uses Agent SDK (no API key needed)
945
- */
946
- async curateWithAnthropicSDK(
947
- messages: Array<{ role: "user" | "assistant"; content: string | any[] }>,
948
- triggerType: CurationTrigger = "session_end",
949
- ): Promise<CurationResult> {
950
- if (!this._config.apiKey) {
951
- throw new Error(
952
- "API key required for Anthropic SDK mode. Set ANTHROPIC_API_KEY environment variable.",
953
- );
954
- }
955
-
956
- // Dynamic import to make SDK optional
957
- const { default: Anthropic } = await import("@anthropic-ai/sdk");
958
- const client = new Anthropic({ apiKey: this._config.apiKey });
959
-
960
- const systemPrompt = this.buildCurationPrompt(triggerType);
961
-
962
- // Build the conversation: original messages + curation request
963
- const conversationMessages = [
964
- ...messages,
965
- {
966
- role: "user" as const,
967
- content:
968
- "This session has ended. Please curate the memories from our conversation according to your system instructions. Return ONLY the JSON structure with no additional text.",
969
- },
970
- ];
971
-
972
- const response = await client.messages.create({
973
- model: "claude-sonnet-4-20250514",
974
- max_tokens: 64000,
975
- system: systemPrompt,
976
- messages: conversationMessages,
977
- });
978
-
979
- const content = response.content[0];
980
- if (content.type !== "text") {
981
- throw new Error("Unexpected response type from Claude API");
982
- }
983
-
984
- return this.parseCurationResponse(content.text);
985
- }
986
-
987
969
  /**
988
970
  * Curate from a parsed session segment
989
971
  * Convenience method that extracts messages from SessionSegment
@@ -997,44 +979,6 @@ This session has ended. Please curate the memories from this conversation accord
997
979
  return this.curateWithSDK(segment.messages, triggerType);
998
980
  }
999
981
 
1000
- /**
1001
- * Find and curate from a session file directly (LEGACY - no segmentation)
1002
- * Uses SDK mode to avoid CLI output truncation issues
1003
- * @deprecated Use curateFromSessionFileWithSegments() for large sessions
1004
- */
1005
- async curateFromSessionFile(
1006
- sessionId: string,
1007
- triggerType: CurationTrigger = "session_end",
1008
- cwd?: string,
1009
- ): Promise<CurationResult> {
1010
- // Find the session file
1011
- const sessionFile = await this._findSessionFile(sessionId, cwd);
1012
- if (!sessionFile) {
1013
- logger.debug(
1014
- `Curator: Could not find session file for ${sessionId}`,
1015
- "curator",
1016
- );
1017
- return { session_summary: "", memories: [] };
1018
- }
1019
-
1020
- logger.debug(`Curator: Found session file: ${sessionFile}`, "curator");
1021
-
1022
- // Parse the session
1023
- const session = await parseSessionFile(sessionFile);
1024
- if (session.messages.length === 0) {
1025
- logger.debug("Curator: Session has no messages", "curator");
1026
- return { session_summary: "", memories: [] };
1027
- }
1028
-
1029
- logger.debug(
1030
- `Curator: Parsed ${session.messages.length} messages, ~${session.metadata.estimatedTokens} tokens`,
1031
- "curator",
1032
- );
1033
-
1034
- // Use SDK mode with the parsed messages
1035
- return this.curateWithSDK(session.messages as any, triggerType);
1036
- }
1037
-
1038
982
  /**
1039
983
  * Find and curate from a session file with SEGMENTATION
1040
984
  * Breaks large sessions into segments and curates each one
@@ -1062,43 +1006,50 @@ This session has ended. Please curate the memories from this conversation accord
1062
1006
  const sessionFile = await this._findSessionFile(sessionId, cwd);
1063
1007
  if (!sessionFile) {
1064
1008
  logger.debug(
1065
- `Curator: Could not find session file for ${sessionId}`,
1009
+ `Curator Claude - Could not find session file for ${sessionId}`,
1066
1010
  "curator",
1067
1011
  );
1068
1012
  return { session_summary: "", memories: [] };
1069
1013
  }
1070
1014
 
1071
- logger.debug(`Curator: Found session file: ${sessionFile}`, "curator");
1015
+ logger.debug(
1016
+ `Curator Claude - Found session file: ${sessionFile}`,
1017
+ "curator",
1018
+ );
1072
1019
 
1073
1020
  // Parse the session to get metadata first
1074
1021
  const session = await parseSessionFile(sessionFile);
1075
1022
  if (session.messages.length === 0) {
1076
- logger.debug("Curator: Session has no messages", "curator");
1023
+ logger.debug("Curator Claude - Session has no messages", "curator");
1077
1024
  return { session_summary: "", memories: [] };
1078
1025
  }
1079
1026
 
1080
1027
  // Log detailed session stats
1081
1028
  const { metadata } = session;
1082
1029
  logger.debug(
1083
- `Curator: Session stats - ${metadata.messageCount} messages, ${metadata.toolUseCount} tool_use, ${metadata.toolResultCount} tool_result, thinking: ${metadata.hasThinkingBlocks}, images: ${metadata.hasImages}`,
1030
+ `Curator Claude - Session stats - ${metadata.messageCount} messages, ${metadata.toolUseCount} tool_use, ${metadata.toolResultCount} tool_result, thinking: ${metadata.hasThinkingBlocks}, images: ${metadata.hasImages}`,
1084
1031
  "curator",
1085
1032
  );
1086
1033
  logger.debug(
1087
- `Curator: Estimated ${metadata.estimatedTokens} tokens, file size ${Math.round(metadata.fileSize / 1024)}KB`,
1034
+ `Curator Claude - Estimated ${metadata.estimatedTokens} tokens, file size ${Math.round(metadata.fileSize / 1024)}KB`,
1088
1035
  "curator",
1089
1036
  );
1090
1037
 
1091
1038
  // Parse into segments using the same function as ingest
1092
- const { parseSessionFileWithSegments } = await import("./session-parser.ts");
1093
- const segments = await parseSessionFileWithSegments(sessionFile, maxTokensPerSegment);
1039
+ const { parseSessionFileWithSegments } =
1040
+ await import("./session-parser.ts");
1041
+ const segments = await parseSessionFileWithSegments(
1042
+ sessionFile,
1043
+ maxTokensPerSegment,
1044
+ );
1094
1045
 
1095
1046
  if (segments.length === 0) {
1096
- logger.debug("Curator: No segments found in session", "curator");
1047
+ logger.debug("Curator Claude - No segments found in session", "curator");
1097
1048
  return { session_summary: "", memories: [] };
1098
1049
  }
1099
1050
 
1100
1051
  logger.debug(
1101
- `Curator: Split into ${segments.length} segment(s) at ~${Math.round(maxTokensPerSegment / 1000)}k tokens each`,
1052
+ `Curator Claude - Split into ${segments.length} segment(s) at ~${Math.round(maxTokensPerSegment / 1000)}k tokens each`,
1102
1053
  "curator",
1103
1054
  );
1104
1055
 
@@ -1106,7 +1057,8 @@ This session has ended. Please curate the memories from this conversation accord
1106
1057
  const allMemories: CuratedMemory[] = [];
1107
1058
  const sessionSummaries: string[] = [];
1108
1059
  const interactionTones: string[] = [];
1109
- const projectSnapshots: NonNullable<CurationResult["project_snapshot"]>[] = [];
1060
+ const projectSnapshots: NonNullable<CurationResult["project_snapshot"]>[] =
1061
+ [];
1110
1062
  let failedSegments = 0;
1111
1063
 
1112
1064
  // Curate each segment
@@ -1115,7 +1067,7 @@ This session has ended. Please curate the memories from this conversation accord
1115
1067
  const tokensLabel = `${Math.round(segment.estimatedTokens / 1000)}k`;
1116
1068
 
1117
1069
  logger.debug(
1118
- `Curator: Processing segment ${segmentLabel} (${segment.messages.length} messages, ~${tokensLabel} tokens)`,
1070
+ `Curator Claude - Processing segment ${segmentLabel} (${segment.messages.length} messages, ~${tokensLabel} tokens)`,
1119
1071
  "curator",
1120
1072
  );
1121
1073
 
@@ -1138,7 +1090,7 @@ This session has ended. Please curate the memories from this conversation accord
1138
1090
  }
1139
1091
 
1140
1092
  logger.debug(
1141
- `Curator: Segment ${segmentLabel} extracted ${result.memories.length} memories`,
1093
+ `Curator Claude - Segment ${segmentLabel} extracted ${result.memories.length} memories`,
1142
1094
  "curator",
1143
1095
  );
1144
1096
 
@@ -1154,7 +1106,7 @@ This session has ended. Please curate the memories from this conversation accord
1154
1106
  } catch (error: any) {
1155
1107
  failedSegments++;
1156
1108
  logger.debug(
1157
- `Curator: Segment ${segmentLabel} failed: ${error.message}`,
1109
+ `Curator Claude - Segment ${segmentLabel} failed: ${error.message}`,
1158
1110
  "curator",
1159
1111
  );
1160
1112
  }
@@ -1163,16 +1115,16 @@ This session has ended. Please curate the memories from this conversation accord
1163
1115
  // Log final summary
1164
1116
  if (failedSegments > 0) {
1165
1117
  logger.debug(
1166
- `Curator: Completed with ${failedSegments} failed segment(s)`,
1118
+ `Curator Claude - Completed with ${failedSegments} failed segment(s)`,
1167
1119
  "curator",
1168
1120
  );
1169
1121
  }
1170
1122
  logger.debug(
1171
- `Curator: Total ${allMemories.length} memories from ${segments.length} segment(s)`,
1123
+ `Curator Claude - Total ${allMemories.length} memories from ${segments.length} segment(s)`,
1172
1124
  "curator",
1173
1125
  );
1174
1126
  logger.debug(
1175
- `Curator: Collected ${sessionSummaries.length} summaries, ${projectSnapshots.length} snapshots`,
1127
+ `Curator Claude - Collected ${sessionSummaries.length} summaries, ${projectSnapshots.length} snapshots`,
1176
1128
  "curator",
1177
1129
  );
1178
1130
 
@@ -1189,9 +1141,10 @@ This session has ended. Please curate the memories from this conversation accord
1189
1141
  }
1190
1142
 
1191
1143
  // For interaction tone, use the most common one or the last one
1192
- const finalTone = interactionTones.length > 0
1193
- ? interactionTones[interactionTones.length - 1]
1194
- : undefined;
1144
+ const finalTone =
1145
+ interactionTones.length > 0
1146
+ ? interactionTones[interactionTones.length - 1]
1147
+ : undefined;
1195
1148
 
1196
1149
  // For project snapshot, merge all snapshots - later ones take precedence for phase,
1197
1150
  // but accumulate achievements/challenges/next_steps
@@ -1202,8 +1155,10 @@ This session has ended. Please curate the memories from this conversation accord
1202
1155
  const allNextSteps: string[] = [];
1203
1156
 
1204
1157
  for (const snap of projectSnapshots) {
1205
- if (snap.recent_achievements) allAchievements.push(...snap.recent_achievements);
1206
- if (snap.active_challenges) allChallenges.push(...snap.active_challenges);
1158
+ if (snap.recent_achievements)
1159
+ allAchievements.push(...snap.recent_achievements);
1160
+ if (snap.active_challenges)
1161
+ allChallenges.push(...snap.active_challenges);
1207
1162
  if (snap.next_steps) allNextSteps.push(...snap.next_steps);
1208
1163
  }
1209
1164
 
@@ -1277,267 +1232,6 @@ This session has ended. Please curate the memories from this conversation accord
1277
1232
 
1278
1233
  return null;
1279
1234
  }
1280
-
1281
- /**
1282
- * Curate using CLI subprocess (for hook mode)
1283
- * Resumes a session and asks it to curate
1284
- */
1285
- async curateWithCLI(
1286
- sessionId: string,
1287
- triggerType: CurationTrigger = "session_end",
1288
- cwd?: string,
1289
- cliTypeOverride?: "claude-code" | "gemini-cli",
1290
- ): Promise<CurationResult> {
1291
- const type = cliTypeOverride ?? this._config.cliType;
1292
- const systemPrompt = this.buildCurationPrompt(triggerType);
1293
- const userMessage =
1294
- "This session has ended. Please curate the memories from our conversation according to the instructions in your system prompt. Return ONLY the JSON structure.";
1295
-
1296
- // Build CLI command based on type
1297
- const args: string[] = [];
1298
- let command = this._config.cliCommand;
1299
-
1300
- if (type === "claude-code") {
1301
- args.push(
1302
- "--resume",
1303
- sessionId,
1304
- "-p",
1305
- userMessage,
1306
- "--append-system-prompt",
1307
- systemPrompt,
1308
- "--output-format",
1309
- "json",
1310
- );
1311
- } else {
1312
- // gemini-cli
1313
- command = "gemini"; // Default to 'gemini' in PATH for gemini-cli
1314
- args.push(
1315
- "--resume",
1316
- sessionId,
1317
- "-p",
1318
- `${systemPrompt}\n\n${userMessage}`,
1319
- "--output-format",
1320
- "json",
1321
- );
1322
- }
1323
-
1324
- // Execute CLI
1325
- logger.debug(
1326
- `Curator: Spawning CLI with CLAUDE_CODE_MAX_OUTPUT_TOKENS=64000`,
1327
- "curator",
1328
- );
1329
- logger.debug(
1330
- `Curator: Command: ${command} ${args.slice(0, 3).join(" ")}...`,
1331
- "curator",
1332
- );
1333
-
1334
- const proc = Bun.spawn([command, ...args], {
1335
- cwd,
1336
- env: {
1337
- ...process.env,
1338
- MEMORY_CURATOR_ACTIVE: "1", // Prevent recursive hook triggering
1339
- CLAUDE_CODE_MAX_OUTPUT_TOKENS: "64000", // Max output to avoid truncation
1340
- },
1341
- stdout: "pipe",
1342
- stderr: "pipe",
1343
- });
1344
-
1345
- // Capture both stdout and stderr
1346
- const [stdout, stderr] = await Promise.all([
1347
- new Response(proc.stdout).text(),
1348
- new Response(proc.stderr).text(),
1349
- ]);
1350
- const exitCode = await proc.exited;
1351
-
1352
- logger.debug(`Curator CLI exit code: ${exitCode}`, "curator");
1353
- if (stderr && stderr.trim()) {
1354
- logger.debug(
1355
- `Curator stderr (${stderr.length} chars): ${stderr}`,
1356
- "curator",
1357
- );
1358
- }
1359
-
1360
- if (exitCode !== 0) {
1361
- return { session_summary: "", memories: [] };
1362
- }
1363
-
1364
- // Log raw response in verbose mode
1365
- logger.debug(`Curator CLI raw stdout (${stdout.length} chars):`, "curator");
1366
- // Always log the last 100 chars to see where output ends
1367
- logger.debug(`Curator: '${stdout}'`, "curator");
1368
- if (logger.isVerbose()) {
1369
- // Show first 2000 chars to avoid flooding console
1370
- const preview = stdout.length > 2000 ? stdout : stdout;
1371
- console.log(preview);
1372
- }
1373
-
1374
- // Extract JSON from CLI output
1375
- try {
1376
- // First, parse the CLI JSON wrapper
1377
- const cliOutput = JSON.parse(stdout);
1378
-
1379
- // Claude Code now returns an array of events - find the result object
1380
- let resultObj: any;
1381
- if (Array.isArray(cliOutput)) {
1382
- // New format: array of events, find the one with type="result"
1383
- resultObj = cliOutput.find((item: any) => item.type === "result");
1384
- if (!resultObj) {
1385
- logger.debug(
1386
- "Curator: No result object found in CLI output array",
1387
- "curator",
1388
- );
1389
- return { session_summary: "", memories: [] };
1390
- }
1391
- } else {
1392
- // Old format: single object (backwards compatibility)
1393
- resultObj = cliOutput;
1394
- }
1395
-
1396
- // Check for error response FIRST (like Python does)
1397
- if (resultObj.type === "error" || resultObj.is_error === true) {
1398
- logger.debug(
1399
- `Curator: Error response from CLI: ${JSON.stringify(resultObj)}`,
1400
- "curator",
1401
- );
1402
- return { session_summary: "", memories: [] };
1403
- }
1404
-
1405
- // Extract the "result" field (AI's response text)
1406
- let aiResponse = "";
1407
- if (typeof resultObj.result === "string") {
1408
- aiResponse = resultObj.result;
1409
- } else {
1410
- logger.debug(
1411
- `Curator: result field is not a string: ${typeof resultObj.result}`,
1412
- "curator",
1413
- );
1414
- return { session_summary: "", memories: [] };
1415
- }
1416
-
1417
- // Log the AI response in verbose mode
1418
- logger.debug(
1419
- `Curator AI response (${aiResponse.length} chars):`,
1420
- "curator",
1421
- );
1422
- if (logger.isVerbose()) {
1423
- const preview = aiResponse.length > 3000 ? aiResponse : aiResponse;
1424
- console.log(preview);
1425
- }
1426
-
1427
- // Remove markdown code blocks if present (```json ... ```)
1428
- const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
1429
- if (codeBlockMatch) {
1430
- logger.debug(
1431
- `Curator: Code block matched, extracting ${codeBlockMatch[1]!.length} chars`,
1432
- "curator",
1433
- );
1434
- aiResponse = codeBlockMatch[1]!.trim();
1435
- } else {
1436
- logger.debug(
1437
- `Curator: No code block found, using raw response`,
1438
- "curator",
1439
- );
1440
- // Log the last 200 chars to see where truncation happened
1441
- if (aiResponse.length > 200) {
1442
- logger.debug(`Curator: ${aiResponse}`, "curator");
1443
- }
1444
- }
1445
-
1446
- // Now find the JSON object (same regex as Python)
1447
- const jsonMatch = aiResponse.match(/\{[\s\S]*\}/)?.[0];
1448
- if (jsonMatch) {
1449
- logger.debug(
1450
- `Curator: Found JSON object (${jsonMatch.length} chars), parsing...`,
1451
- "curator",
1452
- );
1453
-
1454
- // Detect likely truncation: JSON much smaller than response
1455
- const likelyTruncated = jsonMatch.length < aiResponse.length * 0.5;
1456
-
1457
- if (likelyTruncated) {
1458
- logger.debug(
1459
- `Curator: WARNING - JSON (${jsonMatch.length}) much smaller than response (${aiResponse.length}) - likely truncated`,
1460
- "curator",
1461
- );
1462
- // Find the last } position and log what's around it
1463
- const lastBrace = aiResponse.lastIndexOf("}");
1464
- logger.debug(
1465
- `Curator: Last } at position ${lastBrace}, char before: '${aiResponse[lastBrace - 1]}', char after: '${aiResponse[lastBrace + 1] || "EOF"}'`,
1466
- "curator",
1467
- );
1468
- // Log chars around the cut point
1469
- const cutPoint = jsonMatch.length;
1470
- logger.debug(
1471
- `Curator: Around match end (${cutPoint}): '...${aiResponse.slice(Math.max(0, cutPoint - 50), cutPoint + 50)}...'`,
1472
- "curator",
1473
- );
1474
- }
1475
-
1476
- const result = this.parseCurationResponse(jsonMatch);
1477
-
1478
- // If we got 0 memories and likely truncated, try SDK fallback
1479
- if (result.memories.length === 0 && likelyTruncated) {
1480
- logger.debug(
1481
- "Curator: CLI mode returned 0 memories with truncation detected, trying SDK fallback...",
1482
- "curator",
1483
- );
1484
- return this._fallbackToSDK(sessionId, triggerType, cwd);
1485
- }
1486
-
1487
- return result;
1488
- } else {
1489
- logger.debug("Curator: No JSON object found in AI response", "curator");
1490
- }
1491
- } catch (error: any) {
1492
- // Parse error - return empty result
1493
- logger.debug(`Curator: Parse error: ${error.message}`, "curator");
1494
- }
1495
-
1496
- // CLI mode failed - try SDK fallback
1497
- logger.debug("Curator: CLI mode failed, trying SDK fallback...", "curator");
1498
- return this._fallbackToSDK(sessionId, triggerType, cwd);
1499
- }
1500
-
1501
- /**
1502
- * Fallback to SDK mode when CLI mode fails (e.g., output truncation)
1503
- * Now uses segmented approach for large sessions
1504
- */
1505
- private async _fallbackToSDK(
1506
- sessionId: string,
1507
- triggerType: CurationTrigger,
1508
- cwd?: string,
1509
- ): Promise<CurationResult> {
1510
- try {
1511
- // Use segmented approach - same as ingest command
1512
- const result = await this.curateFromSessionFileWithSegments(
1513
- sessionId,
1514
- triggerType,
1515
- cwd,
1516
- 150000, // 150k tokens per segment
1517
- (progress) => {
1518
- logger.debug(
1519
- `Curator fallback: Segment ${progress.segmentIndex + 1}/${progress.totalSegments} → ${progress.memoriesExtracted} memories`,
1520
- "curator",
1521
- );
1522
- },
1523
- );
1524
- if (result.memories.length > 0) {
1525
- logger.debug(
1526
- `Curator: SDK fallback succeeded with ${result.memories.length} memories`,
1527
- "curator",
1528
- );
1529
- } else {
1530
- logger.debug(
1531
- "Curator: SDK fallback also returned 0 memories",
1532
- "curator",
1533
- );
1534
- }
1535
- return result;
1536
- } catch (error: any) {
1537
- logger.debug(`Curator: SDK fallback failed: ${error.message}`, "curator");
1538
- return { session_summary: "", memories: [] };
1539
- }
1540
- }
1541
1235
  }
1542
1236
 
1543
1237
  /**