@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.
- package/README.md +5 -0
- package/dist/index.js +5115 -41504
- package/dist/index.mjs +1425 -10549
- package/dist/server/index.js +3779 -12801
- package/dist/server/index.mjs +3474 -12498
- package/hooks/gemini/curation.ts +2 -1
- package/hooks/gemini/session-start.ts +6 -3
- package/hooks/gemini/user-prompt.ts +5 -7
- package/package.json +1 -1
- package/src/core/curator.ts +142 -448
- package/src/core/manager.ts +323 -546
- package/src/server/index.ts +6 -2
- package/src/utils/paths.ts +191 -0
package/src/core/curator.ts
CHANGED
|
@@ -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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
|
707
|
+
`Curator Claude - Resuming session ${claudeSessionId}`,
|
|
734
708
|
"curator",
|
|
735
709
|
);
|
|
736
710
|
|
|
737
711
|
try {
|
|
738
712
|
const q = query({
|
|
739
|
-
prompt:
|
|
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
|
-
|
|
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 (
|
|
727
|
+
if (
|
|
728
|
+
message.type === "assistant" &&
|
|
729
|
+
"usage" in message &&
|
|
730
|
+
message.usage
|
|
731
|
+
) {
|
|
753
732
|
logger.debug(
|
|
754
|
-
`Curator
|
|
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
|
|
740
|
+
if (message.subtype !== "success") {
|
|
762
741
|
logger.debug(
|
|
763
|
-
`Curator
|
|
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
|
|
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
|
-
|
|
781
|
-
|
|
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
|
|
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
|
|
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 =
|
|
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",
|
|
832
|
-
"
|
|
833
|
-
"
|
|
804
|
+
"--resume",
|
|
805
|
+
"latest",
|
|
806
|
+
"-p",
|
|
807
|
+
userMessage,
|
|
808
|
+
"--output-format",
|
|
809
|
+
"json",
|
|
834
810
|
];
|
|
835
811
|
|
|
836
|
-
logger.debug(
|
|
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(
|
|
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(
|
|
874
|
-
|
|
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] ===
|
|
883
|
-
if (stdout[i] ===
|
|
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(
|
|
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(
|
|
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(
|
|
902
|
+
logger.debug(
|
|
903
|
+
`Curator Gemini - Parsed outer JSON successfully`,
|
|
904
|
+
"curator",
|
|
905
|
+
);
|
|
902
906
|
} catch (outerError: any) {
|
|
903
|
-
logger.debug(
|
|
904
|
-
|
|
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
|
|
923
|
+
logger.debug("Curator Gemini - No response field in output", "curator");
|
|
914
924
|
return { session_summary: "", memories: [] };
|
|
915
925
|
}
|
|
916
926
|
|
|
917
|
-
logger.debug(
|
|
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(
|
|
937
|
+
logger.debug(
|
|
938
|
+
`Curator Gemini - Extracted JSON from code block (${cleanResponse.length} chars)`,
|
|
939
|
+
"curator",
|
|
940
|
+
);
|
|
925
941
|
} else {
|
|
926
|
-
logger.debug(
|
|
942
|
+
logger.debug(
|
|
943
|
+
`Curator Gemini - No code block found, using raw response`,
|
|
944
|
+
"curator",
|
|
945
|
+
);
|
|
927
946
|
}
|
|
928
947
|
|
|
929
|
-
logger.debug(
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
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
|
|
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 } =
|
|
1093
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1118
|
+
`Curator Claude - Completed with ${failedSegments} failed segment(s)`,
|
|
1167
1119
|
"curator",
|
|
1168
1120
|
);
|
|
1169
1121
|
}
|
|
1170
1122
|
logger.debug(
|
|
1171
|
-
`Curator
|
|
1123
|
+
`Curator Claude - Total ${allMemories.length} memories from ${segments.length} segment(s)`,
|
|
1172
1124
|
"curator",
|
|
1173
1125
|
);
|
|
1174
1126
|
logger.debug(
|
|
1175
|
-
`Curator
|
|
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 =
|
|
1193
|
-
|
|
1194
|
-
|
|
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)
|
|
1206
|
-
|
|
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
|
/**
|