@rlabs-inc/memory 0.5.2 → 0.5.8

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,34 +787,40 @@ 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,
812
791
  ): Promise<CurationResult> {
813
792
  const systemPrompt = this.buildCurationPrompt(triggerType);
814
793
  const userMessage =
815
794
  "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
795
 
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
-
796
+ // Write system prompt to temp file (tmpdir always exists)
797
+ const tempPromptPath = getCuratorPromptPath();
827
798
  await Bun.write(tempPromptPath, systemPrompt);
828
799
 
829
800
  // Build CLI command
801
+ // Use --resume latest since SessionEnd hook fires immediately after session ends
830
802
  const args = [
831
- "--resume", sessionId,
832
- "-p", userMessage,
833
- "--output-format", "json",
803
+ "--resume",
804
+ "latest",
805
+ "-p",
806
+ userMessage,
807
+ "--output-format",
808
+ "json",
834
809
  ];
835
810
 
836
- logger.debug(`Curator Gemini: Spawning gemini CLI with session ${sessionId}`, "curator");
811
+ logger.debug(
812
+ `Curator Gemini - Spawning gemini CLI to resume latest session (triggered by ${sessionId})`,
813
+ "curator",
814
+ );
815
+ logger.debug(
816
+ `Curator Gemini - cwd = '${cwd}' | (type: ${typeof cwd})`,
817
+ "curator",
818
+ );
837
819
 
838
820
  // Execute CLI with system prompt via environment variable
821
+ // Must run from original project directory so --resume latest finds correct session
839
822
  const proc = Bun.spawn(["gemini", ...args], {
823
+ cwd: cwd || undefined,
840
824
  env: {
841
825
  ...process.env,
842
826
  MEMORY_CURATOR_ACTIVE: "1", // Prevent recursive hook triggering
@@ -855,11 +839,14 @@ This session has ended. Please curate the memories from this conversation accord
855
839
 
856
840
  logger.debug(`Curator Gemini: Exit code ${exitCode}`, "curator");
857
841
  if (stderr && stderr.trim()) {
858
- logger.debug(`Curator Gemini stderr: ${stderr}`, "curator");
842
+ logger.debug(`Curator Gemini - stderr: ${stderr}`, "curator");
859
843
  }
860
844
 
861
845
  if (exitCode !== 0) {
862
- logger.debug(`Curator Gemini: Failed with exit code ${exitCode}`, "curator");
846
+ logger.debug(
847
+ `Curator Gemini - Failed with exit code ${exitCode}`,
848
+ "curator",
849
+ );
863
850
  return { session_summary: "", memories: [] };
864
851
  }
865
852
 
@@ -868,10 +855,16 @@ This session has ended. Please curate the memories from this conversation accord
868
855
  // We need to extract just the JSON object
869
856
  try {
870
857
  // Find the JSON object - it starts with { and we need to find the matching }
871
- const jsonStart = stdout.indexOf('{');
858
+ const jsonStart = stdout.indexOf("{");
872
859
  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");
860
+ logger.debug(
861
+ "Curator Gemini - No JSON object found in output",
862
+ "curator",
863
+ );
864
+ // logger.debug(
865
+ // `Curator Gemini - Raw stdout: ${stdout.slice(0, 500)}`,
866
+ // "curator",
867
+ // );
875
868
  return { session_summary: "", memories: [] };
876
869
  }
877
870
 
@@ -879,8 +872,8 @@ This session has ended. Please curate the memories from this conversation accord
879
872
  let braceCount = 0;
880
873
  let jsonEnd = -1;
881
874
  for (let i = jsonStart; i < stdout.length; i++) {
882
- if (stdout[i] === '{') braceCount++;
883
- if (stdout[i] === '}') braceCount--;
875
+ if (stdout[i] === "{") braceCount++;
876
+ if (stdout[i] === "}") braceCount--;
884
877
  if (braceCount === 0) {
885
878
  jsonEnd = i + 1;
886
879
  break;
@@ -888,20 +881,35 @@ This session has ended. Please curate the memories from this conversation accord
888
881
  }
889
882
 
890
883
  if (jsonEnd === -1) {
891
- logger.debug("Curator Gemini: Could not find matching closing brace", "curator");
884
+ logger.debug(
885
+ "Curator Gemini - Could not find matching closing brace",
886
+ "curator",
887
+ );
892
888
  return { session_summary: "", memories: [] };
893
889
  }
894
890
 
895
891
  const jsonStr = stdout.slice(jsonStart, jsonEnd);
896
- logger.debug(`Curator Gemini: Extracted JSON (${jsonStr.length} chars) from position ${jsonStart} to ${jsonEnd}`, "curator");
892
+ logger.debug(
893
+ `Curator Gemini - Extracted JSON (${jsonStr.length} chars) from position ${jsonStart} to ${jsonEnd}`,
894
+ "curator",
895
+ );
897
896
 
898
897
  let geminiOutput;
899
898
  try {
900
899
  geminiOutput = JSON.parse(jsonStr);
901
- logger.debug(`Curator Gemini: Parsed outer JSON successfully`, "curator");
900
+ logger.debug(
901
+ `Curator Gemini - Parsed outer JSON successfully`,
902
+ "curator",
903
+ );
902
904
  } 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");
905
+ logger.debug(
906
+ `Curator Gemini - Outer JSON parse failed: ${outerError.message}`,
907
+ "curator",
908
+ );
909
+ // logger.debug(
910
+ // `Curator Gemini - JSON string (first 500): ${jsonStr.slice(0, 500)}`,
911
+ // "curator",
912
+ // );
905
913
  return { session_summary: "", memories: [] };
906
914
  }
907
915
 
@@ -910,80 +918,52 @@ This session has ended. Please curate the memories from this conversation accord
910
918
  const aiResponse = geminiOutput.response || "";
911
919
 
912
920
  if (!aiResponse) {
913
- logger.debug("Curator Gemini: No response field in output", "curator");
921
+ logger.debug("Curator Gemini - No response field in output", "curator");
914
922
  return { session_summary: "", memories: [] };
915
923
  }
916
924
 
917
- logger.debug(`Curator Gemini: Got response (${aiResponse.length} chars)`, "curator");
925
+ logger.debug(
926
+ `Curator Gemini - Got response (${aiResponse.length} chars)`,
927
+ "curator",
928
+ );
918
929
 
919
930
  // Remove markdown code blocks if present
920
931
  let cleanResponse = aiResponse;
921
932
  const codeBlockMatch = aiResponse.match(/```(?:json)?\s*([\s\S]*?)```/);
922
933
  if (codeBlockMatch) {
923
934
  cleanResponse = codeBlockMatch[1].trim();
924
- logger.debug(`Curator Gemini: Extracted JSON from code block (${cleanResponse.length} chars)`, "curator");
935
+ logger.debug(
936
+ `Curator Gemini - Extracted JSON from code block (${cleanResponse.length} chars)`,
937
+ "curator",
938
+ );
925
939
  } else {
926
- logger.debug(`Curator Gemini: No code block found, using raw response`, "curator");
940
+ logger.debug(
941
+ `Curator Gemini - No code block found, using raw response`,
942
+ "curator",
943
+ );
927
944
  }
928
945
 
929
- logger.debug(`Curator Gemini: Calling parseCurationResponse...`, "curator");
946
+ logger.debug(
947
+ `Curator Gemini - Calling parseCurationResponse...`,
948
+ "curator",
949
+ );
930
950
  // Use existing parser
931
951
  const result = this.parseCurationResponse(cleanResponse);
932
- logger.debug(`Curator Gemini: Parsed ${result.memories.length} memories`, "curator");
952
+ logger.debug(
953
+ `Curator Gemini - Parsed ${result.memories.length} memories`,
954
+ "curator",
955
+ );
933
956
  return result;
934
957
  } catch (error: any) {
935
958
  logger.debug(`Curator Gemini: Parse error: ${error.message}`, "curator");
936
- logger.debug(`Curator Gemini: Raw stdout (first 500 chars): ${stdout.slice(0, 500)}`, "curator");
959
+ // logger.debug(
960
+ // `Curator Gemini - Raw stdout (first 500 chars): ${stdout.slice(0, 500)}`,
961
+ // "curator",
962
+ // );
937
963
  return { session_summary: "", memories: [] };
938
964
  }
939
965
  }
940
966
 
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
967
  /**
988
968
  * Curate from a parsed session segment
989
969
  * Convenience method that extracts messages from SessionSegment
@@ -997,44 +977,6 @@ This session has ended. Please curate the memories from this conversation accord
997
977
  return this.curateWithSDK(segment.messages, triggerType);
998
978
  }
999
979
 
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
980
  /**
1039
981
  * Find and curate from a session file with SEGMENTATION
1040
982
  * Breaks large sessions into segments and curates each one
@@ -1062,43 +1004,50 @@ This session has ended. Please curate the memories from this conversation accord
1062
1004
  const sessionFile = await this._findSessionFile(sessionId, cwd);
1063
1005
  if (!sessionFile) {
1064
1006
  logger.debug(
1065
- `Curator: Could not find session file for ${sessionId}`,
1007
+ `Curator Claude - Could not find session file for ${sessionId}`,
1066
1008
  "curator",
1067
1009
  );
1068
1010
  return { session_summary: "", memories: [] };
1069
1011
  }
1070
1012
 
1071
- logger.debug(`Curator: Found session file: ${sessionFile}`, "curator");
1013
+ logger.debug(
1014
+ `Curator Claude - Found session file: ${sessionFile}`,
1015
+ "curator",
1016
+ );
1072
1017
 
1073
1018
  // Parse the session to get metadata first
1074
1019
  const session = await parseSessionFile(sessionFile);
1075
1020
  if (session.messages.length === 0) {
1076
- logger.debug("Curator: Session has no messages", "curator");
1021
+ logger.debug("Curator Claude - Session has no messages", "curator");
1077
1022
  return { session_summary: "", memories: [] };
1078
1023
  }
1079
1024
 
1080
1025
  // Log detailed session stats
1081
1026
  const { metadata } = session;
1082
1027
  logger.debug(
1083
- `Curator: Session stats - ${metadata.messageCount} messages, ${metadata.toolUseCount} tool_use, ${metadata.toolResultCount} tool_result, thinking: ${metadata.hasThinkingBlocks}, images: ${metadata.hasImages}`,
1028
+ `Curator Claude - Session stats - ${metadata.messageCount} messages, ${metadata.toolUseCount} tool_use, ${metadata.toolResultCount} tool_result, thinking: ${metadata.hasThinkingBlocks}, images: ${metadata.hasImages}`,
1084
1029
  "curator",
1085
1030
  );
1086
1031
  logger.debug(
1087
- `Curator: Estimated ${metadata.estimatedTokens} tokens, file size ${Math.round(metadata.fileSize / 1024)}KB`,
1032
+ `Curator Claude - Estimated ${metadata.estimatedTokens} tokens, file size ${Math.round(metadata.fileSize / 1024)}KB`,
1088
1033
  "curator",
1089
1034
  );
1090
1035
 
1091
1036
  // Parse into segments using the same function as ingest
1092
- const { parseSessionFileWithSegments } = await import("./session-parser.ts");
1093
- const segments = await parseSessionFileWithSegments(sessionFile, maxTokensPerSegment);
1037
+ const { parseSessionFileWithSegments } =
1038
+ await import("./session-parser.ts");
1039
+ const segments = await parseSessionFileWithSegments(
1040
+ sessionFile,
1041
+ maxTokensPerSegment,
1042
+ );
1094
1043
 
1095
1044
  if (segments.length === 0) {
1096
- logger.debug("Curator: No segments found in session", "curator");
1045
+ logger.debug("Curator Claude - No segments found in session", "curator");
1097
1046
  return { session_summary: "", memories: [] };
1098
1047
  }
1099
1048
 
1100
1049
  logger.debug(
1101
- `Curator: Split into ${segments.length} segment(s) at ~${Math.round(maxTokensPerSegment / 1000)}k tokens each`,
1050
+ `Curator Claude - Split into ${segments.length} segment(s) at ~${Math.round(maxTokensPerSegment / 1000)}k tokens each`,
1102
1051
  "curator",
1103
1052
  );
1104
1053
 
@@ -1106,7 +1055,8 @@ This session has ended. Please curate the memories from this conversation accord
1106
1055
  const allMemories: CuratedMemory[] = [];
1107
1056
  const sessionSummaries: string[] = [];
1108
1057
  const interactionTones: string[] = [];
1109
- const projectSnapshots: NonNullable<CurationResult["project_snapshot"]>[] = [];
1058
+ const projectSnapshots: NonNullable<CurationResult["project_snapshot"]>[] =
1059
+ [];
1110
1060
  let failedSegments = 0;
1111
1061
 
1112
1062
  // Curate each segment
@@ -1115,7 +1065,7 @@ This session has ended. Please curate the memories from this conversation accord
1115
1065
  const tokensLabel = `${Math.round(segment.estimatedTokens / 1000)}k`;
1116
1066
 
1117
1067
  logger.debug(
1118
- `Curator: Processing segment ${segmentLabel} (${segment.messages.length} messages, ~${tokensLabel} tokens)`,
1068
+ `Curator Claude - Processing segment ${segmentLabel} (${segment.messages.length} messages, ~${tokensLabel} tokens)`,
1119
1069
  "curator",
1120
1070
  );
1121
1071
 
@@ -1138,7 +1088,7 @@ This session has ended. Please curate the memories from this conversation accord
1138
1088
  }
1139
1089
 
1140
1090
  logger.debug(
1141
- `Curator: Segment ${segmentLabel} extracted ${result.memories.length} memories`,
1091
+ `Curator Claude - Segment ${segmentLabel} extracted ${result.memories.length} memories`,
1142
1092
  "curator",
1143
1093
  );
1144
1094
 
@@ -1154,7 +1104,7 @@ This session has ended. Please curate the memories from this conversation accord
1154
1104
  } catch (error: any) {
1155
1105
  failedSegments++;
1156
1106
  logger.debug(
1157
- `Curator: Segment ${segmentLabel} failed: ${error.message}`,
1107
+ `Curator Claude - Segment ${segmentLabel} failed: ${error.message}`,
1158
1108
  "curator",
1159
1109
  );
1160
1110
  }
@@ -1163,16 +1113,16 @@ This session has ended. Please curate the memories from this conversation accord
1163
1113
  // Log final summary
1164
1114
  if (failedSegments > 0) {
1165
1115
  logger.debug(
1166
- `Curator: Completed with ${failedSegments} failed segment(s)`,
1116
+ `Curator Claude - Completed with ${failedSegments} failed segment(s)`,
1167
1117
  "curator",
1168
1118
  );
1169
1119
  }
1170
1120
  logger.debug(
1171
- `Curator: Total ${allMemories.length} memories from ${segments.length} segment(s)`,
1121
+ `Curator Claude - Total ${allMemories.length} memories from ${segments.length} segment(s)`,
1172
1122
  "curator",
1173
1123
  );
1174
1124
  logger.debug(
1175
- `Curator: Collected ${sessionSummaries.length} summaries, ${projectSnapshots.length} snapshots`,
1125
+ `Curator Claude - Collected ${sessionSummaries.length} summaries, ${projectSnapshots.length} snapshots`,
1176
1126
  "curator",
1177
1127
  );
1178
1128
 
@@ -1189,9 +1139,10 @@ This session has ended. Please curate the memories from this conversation accord
1189
1139
  }
1190
1140
 
1191
1141
  // 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;
1142
+ const finalTone =
1143
+ interactionTones.length > 0
1144
+ ? interactionTones[interactionTones.length - 1]
1145
+ : undefined;
1195
1146
 
1196
1147
  // For project snapshot, merge all snapshots - later ones take precedence for phase,
1197
1148
  // but accumulate achievements/challenges/next_steps
@@ -1202,8 +1153,10 @@ This session has ended. Please curate the memories from this conversation accord
1202
1153
  const allNextSteps: string[] = [];
1203
1154
 
1204
1155
  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);
1156
+ if (snap.recent_achievements)
1157
+ allAchievements.push(...snap.recent_achievements);
1158
+ if (snap.active_challenges)
1159
+ allChallenges.push(...snap.active_challenges);
1207
1160
  if (snap.next_steps) allNextSteps.push(...snap.next_steps);
1208
1161
  }
1209
1162
 
@@ -1277,267 +1230,6 @@ This session has ended. Please curate the memories from this conversation accord
1277
1230
 
1278
1231
  return null;
1279
1232
  }
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
1233
  }
1542
1234
 
1543
1235
  /**