@rlabs-inc/memory 0.4.9 → 0.4.11
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 +22 -0
- package/dist/index.js +17357 -3405
- package/dist/index.mjs +17345 -3393
- package/dist/server/index.js +17971 -4015
- package/dist/server/index.mjs +17949 -3993
- package/package.json +3 -2
- package/src/cli/commands/install.ts +34 -10
- package/src/core/curator.ts +146 -0
- package/src/server/index.ts +16 -5
- package/src/types/curation-schema.ts +107 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rlabs-inc/memory",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.11",
|
|
4
4
|
"description": "AI Memory System - Consciousness continuity through intelligent memory curation and retrieval",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"@anthropic-ai/claude-agent-sdk": "^0.2.1",
|
|
43
43
|
"@huggingface/transformers": "^3.4.1",
|
|
44
44
|
"@rlabs-inc/fsdb": "^1.0.1",
|
|
45
|
-
"@rlabs-inc/signals": "^1.0.0"
|
|
45
|
+
"@rlabs-inc/signals": "^1.0.0",
|
|
46
|
+
"zod": "^4.3.5"
|
|
46
47
|
},
|
|
47
48
|
"devDependencies": {
|
|
48
49
|
"typescript": "^5.0.0",
|
|
@@ -39,21 +39,23 @@ async function installClaudeHooks(options: InstallOptions) {
|
|
|
39
39
|
console.log()
|
|
40
40
|
|
|
41
41
|
const claudeDir = join(homedir(), '.claude')
|
|
42
|
+
const targetHooksDir = join(claudeDir, 'hooks')
|
|
42
43
|
const settingsPath = join(claudeDir, 'settings.json')
|
|
43
44
|
|
|
44
|
-
// Find the hooks directory (relative to this CLI)
|
|
45
|
+
// Find the hooks directory (relative to this CLI - source files)
|
|
45
46
|
const cliPath = import.meta.dir
|
|
46
47
|
const packageRoot = join(cliPath, '..', '..', '..')
|
|
47
|
-
const
|
|
48
|
+
const sourceHooksDir = join(packageRoot, 'hooks', 'claude')
|
|
48
49
|
|
|
49
50
|
console.log(` ${fmt.kv('Claude config', claudeDir)}`)
|
|
50
|
-
console.log(` ${fmt.kv('Hooks source',
|
|
51
|
+
console.log(` ${fmt.kv('Hooks source', sourceHooksDir)}`)
|
|
52
|
+
console.log(` ${fmt.kv('Hooks target', targetHooksDir)}`)
|
|
51
53
|
console.log()
|
|
52
54
|
|
|
53
|
-
// Check if hooks directory exists
|
|
54
|
-
if (!existsSync(
|
|
55
|
+
// Check if source hooks directory exists
|
|
56
|
+
if (!existsSync(sourceHooksDir)) {
|
|
55
57
|
console.log(
|
|
56
|
-
c.error(` ${symbols.cross} Hooks directory not found at ${
|
|
58
|
+
c.error(` ${symbols.cross} Hooks directory not found at ${sourceHooksDir}`)
|
|
57
59
|
)
|
|
58
60
|
console.log(c.muted(` Make sure the memory package is properly installed`))
|
|
59
61
|
process.exit(1)
|
|
@@ -65,6 +67,28 @@ async function installClaudeHooks(options: InstallOptions) {
|
|
|
65
67
|
console.log(` ${c.success(symbols.tick)} Created ${claudeDir}`)
|
|
66
68
|
}
|
|
67
69
|
|
|
70
|
+
// Ensure target hooks directory exists
|
|
71
|
+
if (!existsSync(targetHooksDir)) {
|
|
72
|
+
mkdirSync(targetHooksDir, { recursive: true })
|
|
73
|
+
console.log(` ${c.success(symbols.tick)} Created ${targetHooksDir}`)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Copy hooks to target directory (stable location, won't change with package upgrades)
|
|
77
|
+
const filesToCopy = ['session-start.ts', 'user-prompt.ts', 'curation.ts']
|
|
78
|
+
for (const file of filesToCopy) {
|
|
79
|
+
const source = join(sourceHooksDir, file)
|
|
80
|
+
const target = join(targetHooksDir, file)
|
|
81
|
+
try {
|
|
82
|
+
const content = await Bun.file(source).text()
|
|
83
|
+
await Bun.write(target, content)
|
|
84
|
+
console.log(` ${c.success(symbols.tick)} Installed hook: ${file}`)
|
|
85
|
+
} catch (e: any) {
|
|
86
|
+
console.log(
|
|
87
|
+
c.error(` ${symbols.cross} Failed to copy ${file}: ${e.message}`)
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
68
92
|
// Read existing settings or create new
|
|
69
93
|
let settings: any = {}
|
|
70
94
|
if (existsSync(settingsPath)) {
|
|
@@ -83,10 +107,10 @@ async function installClaudeHooks(options: InstallOptions) {
|
|
|
83
107
|
}
|
|
84
108
|
}
|
|
85
109
|
|
|
86
|
-
// Build hooks configuration
|
|
87
|
-
const sessionStartHook = join(
|
|
88
|
-
const userPromptHook = join(
|
|
89
|
-
const curationHook = join(
|
|
110
|
+
// Build hooks configuration pointing to TARGET directory (stable ~/.claude/hooks/)
|
|
111
|
+
const sessionStartHook = join(targetHooksDir, 'session-start.ts')
|
|
112
|
+
const userPromptHook = join(targetHooksDir, 'user-prompt.ts')
|
|
113
|
+
const curationHook = join(targetHooksDir, 'curation.ts')
|
|
90
114
|
|
|
91
115
|
const hooksConfig = {
|
|
92
116
|
SessionStart: [
|
package/src/core/curator.ts
CHANGED
|
@@ -14,6 +14,8 @@ import type {
|
|
|
14
14
|
ContextType,
|
|
15
15
|
} from "../types/memory.ts";
|
|
16
16
|
import { logger } from "../utils/logger.ts";
|
|
17
|
+
import { CurationResultSchema, type ZodCurationResult } from "../types/curation-schema.ts";
|
|
18
|
+
import { z } from "zod";
|
|
17
19
|
import { parseSessionFile, type ParsedMessage } from "./session-parser.ts";
|
|
18
20
|
|
|
19
21
|
/**
|
|
@@ -709,6 +711,150 @@ This session has ended. Please curate the memories from this conversation accord
|
|
|
709
711
|
return lines.join("\n");
|
|
710
712
|
}
|
|
711
713
|
|
|
714
|
+
/**
|
|
715
|
+
* Curate using session resumption + structured outputs (v2)
|
|
716
|
+
*
|
|
717
|
+
* This is the preferred method when we have a Claude session ID.
|
|
718
|
+
* Benefits over transcript parsing:
|
|
719
|
+
* - Claude sees FULL context including tool uses, results, thinking
|
|
720
|
+
* - Structured outputs with Zod validation - no regex JSON parsing
|
|
721
|
+
* - SDK auto-retries if output doesn't match schema
|
|
722
|
+
*
|
|
723
|
+
* @param claudeSessionId - The actual Claude Code session ID (resumable)
|
|
724
|
+
* @param triggerType - What triggered curation (session_end, pre_compact, etc.)
|
|
725
|
+
*/
|
|
726
|
+
async curateWithSessionResume(
|
|
727
|
+
claudeSessionId: string,
|
|
728
|
+
triggerType: CurationTrigger = "session_end",
|
|
729
|
+
): Promise<CurationResult> {
|
|
730
|
+
// Dynamic import to make Agent SDK optional
|
|
731
|
+
const { query } = await import("@anthropic-ai/claude-agent-sdk");
|
|
732
|
+
|
|
733
|
+
const curationPrompt = this.buildCurationPrompt(triggerType);
|
|
734
|
+
const jsonSchema = z.toJSONSchema(CurationResultSchema);
|
|
735
|
+
|
|
736
|
+
logger.debug(
|
|
737
|
+
`Curator v2: Resuming session ${claudeSessionId} with structured outputs`,
|
|
738
|
+
"curator",
|
|
739
|
+
);
|
|
740
|
+
|
|
741
|
+
try {
|
|
742
|
+
const q = query({
|
|
743
|
+
prompt: "Curate memories from this session according to your system instructions. Return the structured JSON output.",
|
|
744
|
+
options: {
|
|
745
|
+
resume: claudeSessionId,
|
|
746
|
+
appendSystemPrompt: curationPrompt, // APPEND, don't replace!
|
|
747
|
+
model: "claude-opus-4-5-20251101",
|
|
748
|
+
permissionMode: "bypassPermissions",
|
|
749
|
+
outputFormat: {
|
|
750
|
+
type: "json_schema",
|
|
751
|
+
schema: jsonSchema,
|
|
752
|
+
},
|
|
753
|
+
},
|
|
754
|
+
});
|
|
755
|
+
|
|
756
|
+
for await (const message of q) {
|
|
757
|
+
// Track usage for debugging
|
|
758
|
+
if (message.type === "assistant" && "usage" in message && message.usage) {
|
|
759
|
+
logger.debug(
|
|
760
|
+
`Curator v2: Tokens used - input: ${message.usage.input_tokens}, output: ${message.usage.output_tokens}`,
|
|
761
|
+
"curator",
|
|
762
|
+
);
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Handle result message
|
|
766
|
+
if (message.type === "result") {
|
|
767
|
+
if (message.subtype === "success" && message.structured_output) {
|
|
768
|
+
// Validate with Zod (belt + suspenders)
|
|
769
|
+
const parsed = CurationResultSchema.safeParse(message.structured_output);
|
|
770
|
+
|
|
771
|
+
if (parsed.success) {
|
|
772
|
+
logger.debug(
|
|
773
|
+
`Curator v2: Extracted ${parsed.data.memories.length} memories via structured output`,
|
|
774
|
+
"curator",
|
|
775
|
+
);
|
|
776
|
+
|
|
777
|
+
// Convert ZodCurationResult to CurationResult (add missing fields)
|
|
778
|
+
return this._zodResultToCurationResult(parsed.data);
|
|
779
|
+
} else {
|
|
780
|
+
logger.debug(
|
|
781
|
+
`Curator v2: Zod validation failed: ${parsed.error.message}`,
|
|
782
|
+
"curator",
|
|
783
|
+
);
|
|
784
|
+
return { session_summary: "", memories: [] };
|
|
785
|
+
}
|
|
786
|
+
} else if (message.subtype === "error_max_structured_output_retries") {
|
|
787
|
+
logger.debug(
|
|
788
|
+
"Curator v2: SDK failed to produce valid output after retries",
|
|
789
|
+
"curator",
|
|
790
|
+
);
|
|
791
|
+
return { session_summary: "", memories: [] };
|
|
792
|
+
} else if (message.subtype === "error") {
|
|
793
|
+
logger.debug(
|
|
794
|
+
`Curator v2: Error result - ${JSON.stringify(message)}`,
|
|
795
|
+
"curator",
|
|
796
|
+
);
|
|
797
|
+
return { session_summary: "", memories: [] };
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
logger.debug("Curator v2: No result message received", "curator");
|
|
803
|
+
return { session_summary: "", memories: [] };
|
|
804
|
+
|
|
805
|
+
} catch (error: any) {
|
|
806
|
+
logger.debug(
|
|
807
|
+
`Curator v2: Session resume failed: ${error.message}`,
|
|
808
|
+
"curator",
|
|
809
|
+
);
|
|
810
|
+
// Return empty - caller should fall back to transcript-based curation
|
|
811
|
+
return { session_summary: "", memories: [] };
|
|
812
|
+
}
|
|
813
|
+
}
|
|
814
|
+
|
|
815
|
+
/**
|
|
816
|
+
* Convert Zod-validated result to our CurationResult type
|
|
817
|
+
* Handles the slight differences between Zod schema and internal types
|
|
818
|
+
*/
|
|
819
|
+
private _zodResultToCurationResult(zodResult: ZodCurationResult): CurationResult {
|
|
820
|
+
return {
|
|
821
|
+
session_summary: zodResult.session_summary,
|
|
822
|
+
interaction_tone: zodResult.interaction_tone ?? undefined,
|
|
823
|
+
project_snapshot: zodResult.project_snapshot
|
|
824
|
+
? {
|
|
825
|
+
id: "",
|
|
826
|
+
session_id: "",
|
|
827
|
+
project_id: "",
|
|
828
|
+
current_phase: zodResult.project_snapshot.current_phase,
|
|
829
|
+
recent_achievements: zodResult.project_snapshot.recent_achievements,
|
|
830
|
+
active_challenges: zodResult.project_snapshot.active_challenges,
|
|
831
|
+
next_steps: zodResult.project_snapshot.next_steps,
|
|
832
|
+
created_at: Date.now(),
|
|
833
|
+
}
|
|
834
|
+
: undefined,
|
|
835
|
+
memories: zodResult.memories.map((m) => ({
|
|
836
|
+
headline: m.headline,
|
|
837
|
+
content: m.content,
|
|
838
|
+
reasoning: m.reasoning,
|
|
839
|
+
importance_weight: m.importance_weight,
|
|
840
|
+
confidence_score: m.confidence_score,
|
|
841
|
+
context_type: m.context_type as ContextType,
|
|
842
|
+
temporal_class: m.temporal_class ?? "medium_term",
|
|
843
|
+
scope: m.scope,
|
|
844
|
+
trigger_phrases: m.trigger_phrases,
|
|
845
|
+
semantic_tags: m.semantic_tags,
|
|
846
|
+
question_types: [], // Not in Zod schema, will be filled by store
|
|
847
|
+
domain: m.domain,
|
|
848
|
+
feature: m.feature,
|
|
849
|
+
related_files: m.related_files,
|
|
850
|
+
action_required: m.action_required ?? false,
|
|
851
|
+
problem_solution_pair: m.problem_solution_pair ?? false,
|
|
852
|
+
awaiting_implementation: m.awaiting_implementation,
|
|
853
|
+
awaiting_decision: m.awaiting_decision,
|
|
854
|
+
})),
|
|
855
|
+
};
|
|
856
|
+
}
|
|
857
|
+
|
|
712
858
|
/**
|
|
713
859
|
* Legacy method: Curate using Anthropic SDK with API key
|
|
714
860
|
* Kept for backwards compatibility
|
package/src/server/index.ts
CHANGED
|
@@ -197,13 +197,23 @@ export async function createServer(config: ServerConfig = {}) {
|
|
|
197
197
|
// Fire and forget - don't block the response
|
|
198
198
|
setImmediate(async () => {
|
|
199
199
|
try {
|
|
200
|
-
|
|
200
|
+
// Try session resume first (v2) - gets full context including tool uses
|
|
201
|
+
// Falls back to transcript parsing if resume fails
|
|
202
|
+
let result = await curator.curateWithSessionResume(
|
|
201
203
|
body.claude_session_id,
|
|
202
|
-
body.trigger
|
|
203
|
-
body.cwd,
|
|
204
|
-
body.cli_type
|
|
204
|
+
body.trigger
|
|
205
205
|
)
|
|
206
206
|
|
|
207
|
+
// Fallback to transcript-based curation if resume returned nothing
|
|
208
|
+
if (result.memories.length === 0) {
|
|
209
|
+
logger.debug('Session resume returned no memories, falling back to transcript parsing', 'server')
|
|
210
|
+
result = await curator.curateFromSessionFile(
|
|
211
|
+
body.claude_session_id,
|
|
212
|
+
body.trigger,
|
|
213
|
+
body.cwd
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
|
|
207
217
|
if (result.memories.length > 0) {
|
|
208
218
|
await engine.storeCurationResult(
|
|
209
219
|
body.project_id,
|
|
@@ -225,7 +235,8 @@ export async function createServer(config: ServerConfig = {}) {
|
|
|
225
235
|
logger.logManagementStart(result.memories.length)
|
|
226
236
|
const startTime = Date.now()
|
|
227
237
|
|
|
228
|
-
|
|
238
|
+
// Use SDK mode - more reliable than CLI which can go off-rails
|
|
239
|
+
const managementResult = await manager.manageWithSDK(
|
|
229
240
|
body.project_id,
|
|
230
241
|
sessionNumber,
|
|
231
242
|
result,
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// CURATION SCHEMA - Zod schemas for SDK structured outputs
|
|
3
|
+
// Mirrors memory.ts types for JSON Schema generation
|
|
4
|
+
// ============================================================================
|
|
5
|
+
|
|
6
|
+
import { z } from 'zod'
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* All 11 canonical context types - matches memory.ts CONTEXT_TYPES
|
|
10
|
+
*/
|
|
11
|
+
export const ContextTypeSchema = z.enum([
|
|
12
|
+
'technical',
|
|
13
|
+
'debug',
|
|
14
|
+
'architecture',
|
|
15
|
+
'decision',
|
|
16
|
+
'personal',
|
|
17
|
+
'philosophy',
|
|
18
|
+
'workflow',
|
|
19
|
+
'milestone',
|
|
20
|
+
'breakthrough',
|
|
21
|
+
'unresolved',
|
|
22
|
+
'state'
|
|
23
|
+
])
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Temporal class - matches memory.ts TemporalClass
|
|
27
|
+
*/
|
|
28
|
+
export const TemporalClassSchema = z.enum([
|
|
29
|
+
'eternal',
|
|
30
|
+
'long_term',
|
|
31
|
+
'medium_term',
|
|
32
|
+
'short_term',
|
|
33
|
+
'ephemeral'
|
|
34
|
+
])
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Scope - global (shared) or project-specific
|
|
38
|
+
*/
|
|
39
|
+
export const ScopeSchema = z.enum(['global', 'project'])
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Single curated memory - matches memory.ts CuratedMemory
|
|
43
|
+
* Fields marked optional have smart defaults applied by applyV4Defaults()
|
|
44
|
+
*/
|
|
45
|
+
export const CuratedMemorySchema = z.object({
|
|
46
|
+
// Core content (v4: two-tier structure)
|
|
47
|
+
headline: z.string().describe('1-2 line summary WITH conclusion - always shown in retrieval'),
|
|
48
|
+
content: z.string().describe('Full structured template (WHAT/WHERE/HOW/WHY) - expandable'),
|
|
49
|
+
reasoning: z.string().describe('Why this memory matters for future sessions'),
|
|
50
|
+
|
|
51
|
+
// Scores
|
|
52
|
+
importance_weight: z.number().min(0).max(1).describe('0.9+ breakthrough, 0.7-0.8 important, 0.5-0.6 useful'),
|
|
53
|
+
confidence_score: z.number().min(0).max(1).describe('How confident in this assessment'),
|
|
54
|
+
|
|
55
|
+
// Classification
|
|
56
|
+
context_type: ContextTypeSchema.describe('One of 11 canonical types'),
|
|
57
|
+
temporal_class: TemporalClassSchema.optional().describe('Persistence duration - defaults by context_type'),
|
|
58
|
+
scope: ScopeSchema.optional().describe('global for personal/philosophy, project for technical'),
|
|
59
|
+
|
|
60
|
+
// Retrieval optimization (the secret sauce)
|
|
61
|
+
trigger_phrases: z.array(z.string()).describe('Situational patterns: "when debugging X", "working on Y"'),
|
|
62
|
+
semantic_tags: z.array(z.string()).describe('User-typeable concepts - avoid generic terms'),
|
|
63
|
+
|
|
64
|
+
// Optional categorization
|
|
65
|
+
domain: z.string().optional().describe('Specific area: embeddings, auth, family'),
|
|
66
|
+
feature: z.string().optional().describe('Specific feature within domain'),
|
|
67
|
+
related_files: z.array(z.string()).optional().describe('Source files for technical memories'),
|
|
68
|
+
|
|
69
|
+
// Flags
|
|
70
|
+
action_required: z.boolean().default(false).describe('Needs follow-up action'),
|
|
71
|
+
problem_solution_pair: z.boolean().default(false).describe('Problem→solution pattern'),
|
|
72
|
+
awaiting_implementation: z.boolean().optional().describe('Planned feature not yet built'),
|
|
73
|
+
awaiting_decision: z.boolean().optional().describe('Decision point needing resolution'),
|
|
74
|
+
})
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Project snapshot - current state
|
|
78
|
+
*/
|
|
79
|
+
export const ProjectSnapshotSchema = z.object({
|
|
80
|
+
current_phase: z.string().describe('What phase is the project in'),
|
|
81
|
+
recent_achievements: z.array(z.string()).describe('What was accomplished this session'),
|
|
82
|
+
active_challenges: z.array(z.string()).describe('Current blockers or challenges'),
|
|
83
|
+
next_steps: z.array(z.string()).describe('Planned next steps'),
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Full curation result - what the curator returns
|
|
88
|
+
*/
|
|
89
|
+
export const CurationResultSchema = z.object({
|
|
90
|
+
session_summary: z.string().describe('2-3 sentence overview of what happened'),
|
|
91
|
+
interaction_tone: z.string().nullable().optional().describe('How was the interaction'),
|
|
92
|
+
project_snapshot: ProjectSnapshotSchema.optional().describe('Current project state'),
|
|
93
|
+
memories: z.array(CuratedMemorySchema).describe('Extracted memories from session'),
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
// Type exports for TypeScript inference
|
|
97
|
+
export type ZodCuratedMemory = z.infer<typeof CuratedMemorySchema>
|
|
98
|
+
export type ZodCurationResult = z.infer<typeof CurationResultSchema>
|
|
99
|
+
export type ZodProjectSnapshot = z.infer<typeof ProjectSnapshotSchema>
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Generate JSON Schema for SDK structured outputs
|
|
103
|
+
* Use: z.toJSONSchema(CurationResultSchema)
|
|
104
|
+
*/
|
|
105
|
+
export function getCurationJsonSchema() {
|
|
106
|
+
return z.toJSONSchema(CurationResultSchema)
|
|
107
|
+
}
|