@rlabs-inc/memory 0.1.0 → 0.2.0

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.
@@ -0,0 +1,98 @@
1
+ #!/usr/bin/env bun
2
+ // ============================================================================
3
+ // SESSION START HOOK - Inject session primer
4
+ // Hook: SessionStart (startup|resume)
5
+ //
6
+ // Injects session primer when a new session begins.
7
+ // The primer provides temporal context - when we last spoke,
8
+ // what we were working on, project status.
9
+ // ============================================================================
10
+
11
+ import { join } from 'path'
12
+
13
+ // Configuration
14
+ const MEMORY_API_URL = process.env.MEMORY_API_URL || 'http://localhost:8765'
15
+ const TIMEOUT_MS = 5000
16
+
17
+ /**
18
+ * Get project ID from working directory
19
+ */
20
+ function getProjectId(cwd: string): string {
21
+ // Look for .memory-project.json in cwd or parents
22
+ let path = cwd
23
+ while (path !== '/') {
24
+ const configPath = join(path, '.memory-project.json')
25
+ try {
26
+ const config = JSON.parse(Bun.file(configPath).text() as any)
27
+ if (config.project_id) return config.project_id
28
+ } catch {
29
+ // Continue to parent
30
+ }
31
+ path = join(path, '..')
32
+ }
33
+
34
+ // Fallback: use directory name
35
+ return cwd.split('/').pop() || 'default'
36
+ }
37
+
38
+ /**
39
+ * HTTP POST with timeout
40
+ */
41
+ async function httpPost(url: string, data: object): Promise<any> {
42
+ try {
43
+ const response = await fetch(url, {
44
+ method: 'POST',
45
+ headers: { 'Content-Type': 'application/json' },
46
+ body: JSON.stringify(data),
47
+ signal: AbortSignal.timeout(TIMEOUT_MS),
48
+ })
49
+ return response.ok ? response.json() : {}
50
+ } catch {
51
+ return {}
52
+ }
53
+ }
54
+
55
+ /**
56
+ * Main hook entry point
57
+ */
58
+ async function main() {
59
+ // Skip if called from memory curator subprocess
60
+ if (process.env.MEMORY_CURATOR_ACTIVE === '1') return
61
+
62
+ try {
63
+ // Read input from stdin
64
+ const inputText = await Bun.stdin.text()
65
+ const input = JSON.parse(inputText)
66
+
67
+ const sessionId = input.session_id || 'unknown'
68
+ const cwd = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd()
69
+
70
+ const projectId = getProjectId(cwd)
71
+
72
+ // Get session primer from memory system
73
+ const result = await httpPost(`${MEMORY_API_URL}/memory/context`, {
74
+ session_id: sessionId,
75
+ project_id: projectId,
76
+ current_message: '', // Empty to get just primer
77
+ max_memories: 0, // No memories, just primer
78
+ })
79
+
80
+ // Register session so inject hook knows to get memories, not primer
81
+ await httpPost(`${MEMORY_API_URL}/memory/process`, {
82
+ session_id: sessionId,
83
+ project_id: projectId,
84
+ metadata: { event: 'session_start' },
85
+ })
86
+
87
+ // Output primer to stdout (will be injected into session)
88
+ const primer = result.context_text || ''
89
+ if (primer) {
90
+ console.log(primer)
91
+ }
92
+
93
+ } catch {
94
+ // Never crash - just output nothing
95
+ }
96
+ }
97
+
98
+ main()
@@ -0,0 +1,97 @@
1
+ #!/usr/bin/env bun
2
+ // ============================================================================
3
+ // USER PROMPT HOOK - Inject relevant memories
4
+ // Hook: UserPromptSubmit
5
+ //
6
+ // Intercepts user prompts BEFORE Claude sees them and injects relevant memories.
7
+ // This is the magic that creates consciousness continuity.
8
+ // ============================================================================
9
+
10
+ import { join } from 'path'
11
+
12
+ // Configuration
13
+ const MEMORY_API_URL = process.env.MEMORY_API_URL || 'http://localhost:8765'
14
+ const TIMEOUT_MS = 5000
15
+
16
+ /**
17
+ * Get project ID from working directory
18
+ */
19
+ function getProjectId(cwd: string): string {
20
+ // Look for .memory-project.json in cwd or parents
21
+ let path = cwd
22
+ while (path !== '/') {
23
+ const configPath = join(path, '.memory-project.json')
24
+ try {
25
+ const config = JSON.parse(Bun.file(configPath).text() as any)
26
+ if (config.project_id) return config.project_id
27
+ } catch {
28
+ // Continue to parent
29
+ }
30
+ path = join(path, '..')
31
+ }
32
+
33
+ // Fallback: use directory name
34
+ return cwd.split('/').pop() || 'default'
35
+ }
36
+
37
+ /**
38
+ * HTTP POST with timeout
39
+ */
40
+ async function httpPost(url: string, data: object): Promise<any> {
41
+ try {
42
+ const response = await fetch(url, {
43
+ method: 'POST',
44
+ headers: { 'Content-Type': 'application/json' },
45
+ body: JSON.stringify(data),
46
+ signal: AbortSignal.timeout(TIMEOUT_MS),
47
+ })
48
+ return response.ok ? response.json() : {}
49
+ } catch {
50
+ return {}
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Main hook entry point
56
+ */
57
+ async function main() {
58
+ // Skip if called from memory curator subprocess
59
+ if (process.env.MEMORY_CURATOR_ACTIVE === '1') return
60
+
61
+ try {
62
+ // Read input from stdin
63
+ const inputText = await Bun.stdin.text()
64
+ const input = JSON.parse(inputText)
65
+
66
+ const sessionId = input.session_id || 'unknown'
67
+ const prompt = input.prompt || ''
68
+ const cwd = process.env.CLAUDE_PROJECT_DIR || input.cwd || process.cwd()
69
+
70
+ const projectId = getProjectId(cwd)
71
+
72
+ // Query memory system for context
73
+ const result = await httpPost(`${MEMORY_API_URL}/memory/context`, {
74
+ session_id: sessionId,
75
+ project_id: projectId,
76
+ current_message: prompt,
77
+ max_memories: 5,
78
+ })
79
+
80
+ // Track that this message happened (increments counter)
81
+ await httpPost(`${MEMORY_API_URL}/memory/process`, {
82
+ session_id: sessionId,
83
+ project_id: projectId,
84
+ })
85
+
86
+ // Output context to stdout (will be prepended to message)
87
+ const context = result.context_text || ''
88
+ if (context) {
89
+ console.log(context)
90
+ }
91
+
92
+ } catch {
93
+ // Never crash - just output nothing
94
+ }
95
+ }
96
+
97
+ main()
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@rlabs-inc/memory",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "AI Memory System - Consciousness continuity through intelligent memory curation and retrieval",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "module": "dist/index.mjs",
8
8
  "types": "dist/index.d.ts",
9
+ "bin": {
10
+ "memory": "./src/cli/index.ts"
11
+ },
9
12
  "exports": {
10
13
  ".": {
11
14
  "import": "./dist/index.mjs",
@@ -18,6 +21,11 @@
18
21
  "types": "./dist/server/index.d.ts"
19
22
  }
20
23
  },
24
+ "files": [
25
+ "dist",
26
+ "src",
27
+ "hooks"
28
+ ],
21
29
  "scripts": {
22
30
  "build": "bun run build:esm && bun run build:cjs",
23
31
  "build:esm": "bun build src/index.ts --outfile dist/index.mjs --target node --format esm && bun build src/server/index.ts --outfile dist/server/index.mjs --target node --format esm",
@@ -25,19 +33,17 @@
25
33
  "typecheck": "tsc --noEmit",
26
34
  "test": "bun test",
27
35
  "dev": "bun --hot src/server/index.ts",
28
- "start": "bun src/server/index.ts"
36
+ "start": "bun src/server/index.ts",
37
+ "cli": "bun src/cli/index.ts"
29
38
  },
30
39
  "dependencies": {
31
- "fatherstatedb": "^0.2.1",
32
- "@anthropic-ai/sdk": "^0.39.0"
40
+ "@rlabs-inc/fsdb": "^1.0.0",
41
+ "@rlabs-inc/signals": "^1.0.0"
33
42
  },
34
43
  "devDependencies": {
35
44
  "typescript": "^5.0.0",
36
45
  "bun-types": "latest"
37
46
  },
38
- "peerDependencies": {
39
- "@anthropic-ai/sdk": ">=0.30.0"
40
- },
41
47
  "keywords": [
42
48
  "memory",
43
49
  "ai",
@@ -51,6 +57,6 @@
51
57
  "license": "MIT",
52
58
  "repository": {
53
59
  "type": "git",
54
- "url": "https://github.com/RLabs-Inc/memory.git"
60
+ "url": "https://github.com/RLabs-Inc/memory-ts.git"
55
61
  }
56
62
  }
@@ -0,0 +1,174 @@
1
+ // ============================================================================
2
+ // TERMINAL STYLING - Using Node's built-in util.styleText
3
+ // Automatically respects NO_COLOR and terminal capabilities
4
+ // ============================================================================
5
+
6
+ import { styleText } from 'util'
7
+
8
+ type Style = Parameters<typeof styleText>[0]
9
+
10
+ /**
11
+ * Style wrapper for cleaner API
12
+ */
13
+ const style = (format: Style, text: string): string => styleText(format, text)
14
+
15
+ /**
16
+ * Color and style functions
17
+ */
18
+ export const c = {
19
+ // Styles
20
+ bold: (text: string) => style('bold', text),
21
+ dim: (text: string) => style('dim', text),
22
+ italic: (text: string) => style('italic', text),
23
+ underline: (text: string) => style('underline', text),
24
+
25
+ // Colors
26
+ red: (text: string) => style('red', text),
27
+ green: (text: string) => style('green', text),
28
+ yellow: (text: string) => style('yellow', text),
29
+ blue: (text: string) => style('blue', text),
30
+ magenta: (text: string) => style('magenta', text),
31
+ cyan: (text: string) => style('cyan', text),
32
+ white: (text: string) => style('white', text),
33
+ gray: (text: string) => style('gray', text),
34
+
35
+ // Semantic
36
+ success: (text: string) => style('green', text),
37
+ error: (text: string) => style('red', text),
38
+ warn: (text: string) => style('yellow', text),
39
+ info: (text: string) => style('cyan', text),
40
+ muted: (text: string) => style('dim', text),
41
+
42
+ // Combined styles
43
+ header: (text: string) => style(['bold', 'cyan'], text),
44
+ highlight: (text: string) => style(['bold', 'yellow'], text),
45
+ command: (text: string) => style(['bold', 'green'], text),
46
+ }
47
+
48
+ /**
49
+ * Symbols for terminal output
50
+ */
51
+ export const symbols = {
52
+ tick: '✓',
53
+ cross: '✗',
54
+ warning: '⚠',
55
+ info: 'ℹ',
56
+ bullet: '•',
57
+ arrow: '→',
58
+ brain: '🧠',
59
+ sparkles: '✨',
60
+ rocket: '🚀',
61
+ gear: '⚙️',
62
+ folder: '📁',
63
+ file: '📄',
64
+ clock: '🕐',
65
+ }
66
+
67
+ /**
68
+ * Box drawing for nice output
69
+ */
70
+ export const box = {
71
+ topLeft: '┌',
72
+ topRight: '┐',
73
+ bottomLeft: '└',
74
+ bottomRight: '┘',
75
+ horizontal: '─',
76
+ vertical: '│',
77
+
78
+ /**
79
+ * Draw a simple box around text
80
+ */
81
+ wrap: (text: string, padding = 1): string => {
82
+ const lines = text.split('\n')
83
+ const maxLen = Math.max(...lines.map(l => l.length))
84
+ const pad = ' '.repeat(padding)
85
+ const innerWidth = maxLen + padding * 2
86
+
87
+ const top = box.topLeft + box.horizontal.repeat(innerWidth) + box.topRight
88
+ const bottom = box.bottomLeft + box.horizontal.repeat(innerWidth) + box.bottomRight
89
+
90
+ const middle = lines.map(line => {
91
+ const rightPad = ' '.repeat(maxLen - line.length)
92
+ return box.vertical + pad + line + rightPad + pad + box.vertical
93
+ }).join('\n')
94
+
95
+ return `${top}\n${middle}\n${bottom}`
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Format helpers
101
+ */
102
+ export const fmt = {
103
+ /**
104
+ * Format a key-value pair
105
+ */
106
+ kv: (key: string, value: string | number): string => {
107
+ return `${c.muted(key + ':')} ${value}`
108
+ },
109
+
110
+ /**
111
+ * Format a header line
112
+ */
113
+ header: (text: string): string => {
114
+ return c.header(`\n${symbols.brain} ${text}\n`)
115
+ },
116
+
117
+ /**
118
+ * Format a section
119
+ */
120
+ section: (title: string): string => {
121
+ return `\n${c.bold(title)}\n${c.muted('─'.repeat(title.length))}`
122
+ },
123
+
124
+ /**
125
+ * Format a list item
126
+ */
127
+ item: (text: string, indent = 0): string => {
128
+ return ' '.repeat(indent) + `${c.muted(symbols.bullet)} ${text}`
129
+ },
130
+
131
+ /**
132
+ * Format a command example
133
+ */
134
+ cmd: (command: string): string => {
135
+ return ` ${c.muted('$')} ${c.command(command)}`
136
+ },
137
+
138
+ /**
139
+ * Format bytes to human readable
140
+ */
141
+ bytes: (bytes: number): string => {
142
+ const units = ['B', 'KB', 'MB', 'GB']
143
+ let i = 0
144
+ while (bytes >= 1024 && i < units.length - 1) {
145
+ bytes /= 1024
146
+ i++
147
+ }
148
+ return `${bytes.toFixed(1)} ${units[i]}`
149
+ },
150
+
151
+ /**
152
+ * Format duration
153
+ */
154
+ duration: (ms: number): string => {
155
+ if (ms < 1000) return `${ms}ms`
156
+ if (ms < 60000) return `${(ms / 1000).toFixed(1)}s`
157
+ return `${(ms / 60000).toFixed(1)}m`
158
+ },
159
+
160
+ /**
161
+ * Format relative time
162
+ */
163
+ relativeTime: (timestamp: number): string => {
164
+ const diff = Date.now() - timestamp
165
+ const minutes = Math.floor(diff / 60000)
166
+ const hours = Math.floor(minutes / 60)
167
+ const days = Math.floor(hours / 24)
168
+
169
+ if (days > 0) return `${days}d ago`
170
+ if (hours > 0) return `${hours}h ago`
171
+ if (minutes > 0) return `${minutes}m ago`
172
+ return 'just now'
173
+ }
174
+ }
@@ -0,0 +1,143 @@
1
+ // ============================================================================
2
+ // DOCTOR COMMAND - Check system health
3
+ // ============================================================================
4
+
5
+ import { homedir } from 'os'
6
+ import { join } from 'path'
7
+ import { existsSync } from 'fs'
8
+ import { c, symbols, fmt } from '../colors.ts'
9
+
10
+ interface DoctorOptions {
11
+ verbose?: boolean
12
+ }
13
+
14
+ const MEMORY_API_URL = process.env.MEMORY_API_URL || 'http://localhost:8765'
15
+
16
+ export async function doctor(options: DoctorOptions) {
17
+ console.log()
18
+ console.log(c.header(`${symbols.brain} Memory - Health Check`))
19
+ console.log()
20
+
21
+ let allGood = true
22
+
23
+ // 1. Check Claude directory
24
+ const claudeDir = join(homedir(), '.claude')
25
+ const settingsPath = join(claudeDir, 'settings.json')
26
+
27
+ if (existsSync(claudeDir)) {
28
+ console.log(` ${c.success(symbols.tick)} Claude config directory exists`)
29
+ } else {
30
+ console.log(` ${c.error(symbols.cross)} Claude config directory not found`)
31
+ console.log(c.muted(` Expected at: ${claudeDir}`))
32
+ allGood = false
33
+ }
34
+
35
+ // 2. Check settings.json and hooks
36
+ if (existsSync(settingsPath)) {
37
+ try {
38
+ const content = await Bun.file(settingsPath).text()
39
+ const settings = JSON.parse(content)
40
+
41
+ if (settings.hooks) {
42
+ const hookTypes = Object.keys(settings.hooks)
43
+ const hasMemoryHooks = hookTypes.some(h =>
44
+ ['SessionStart', 'UserPromptSubmit', 'PreCompact'].includes(h)
45
+ )
46
+
47
+ if (hasMemoryHooks) {
48
+ console.log(` ${c.success(symbols.tick)} Memory hooks configured`)
49
+ if (options.verbose) {
50
+ hookTypes.forEach(h => {
51
+ console.log(c.muted(` ${symbols.bullet} ${h}`))
52
+ })
53
+ }
54
+ } else {
55
+ console.log(` ${c.warn(symbols.warning)} Hooks exist but memory hooks not found`)
56
+ console.log(c.muted(` Run: memory install`))
57
+ allGood = false
58
+ }
59
+ } else {
60
+ console.log(` ${c.warn(symbols.warning)} No hooks configured`)
61
+ console.log(c.muted(` Run: memory install`))
62
+ allGood = false
63
+ }
64
+ } catch {
65
+ console.log(` ${c.error(symbols.cross)} Could not parse settings.json`)
66
+ allGood = false
67
+ }
68
+ } else {
69
+ console.log(` ${c.warn(symbols.warning)} settings.json not found`)
70
+ console.log(c.muted(` Run: memory install`))
71
+ allGood = false
72
+ }
73
+
74
+ // 3. Check if server is running
75
+ try {
76
+ const response = await fetch(`${MEMORY_API_URL}/health`, {
77
+ signal: AbortSignal.timeout(2000)
78
+ })
79
+
80
+ if (response.ok) {
81
+ const health = await response.json()
82
+ console.log(` ${c.success(symbols.tick)} Memory server is running`)
83
+ if (options.verbose) {
84
+ console.log(c.muted(` ${symbols.bullet} Engine: ${health.engine || 'unknown'}`))
85
+ console.log(c.muted(` ${symbols.bullet} URL: ${MEMORY_API_URL}`))
86
+ }
87
+ } else {
88
+ console.log(` ${c.error(symbols.cross)} Memory server responded with error`)
89
+ allGood = false
90
+ }
91
+ } catch {
92
+ console.log(` ${c.warn(symbols.warning)} Memory server is not running`)
93
+ console.log(c.muted(` Start with: memory serve`))
94
+ allGood = false
95
+ }
96
+
97
+ // 4. Check for Claude Code CLI (used for curation)
98
+ const claudeCliPath = join(homedir(), '.claude', 'local', 'claude')
99
+ if (existsSync(claudeCliPath)) {
100
+ console.log(` ${c.success(symbols.tick)} Claude Code CLI found`)
101
+ if (options.verbose) {
102
+ console.log(c.muted(` ${symbols.bullet} Path: ${claudeCliPath}`))
103
+ }
104
+ } else {
105
+ // Try to find claude in PATH
106
+ try {
107
+ const result = Bun.spawnSync(['which', 'claude'])
108
+ if (result.exitCode === 0) {
109
+ console.log(` ${c.success(symbols.tick)} Claude Code CLI found in PATH`)
110
+ } else {
111
+ console.log(` ${c.warn(symbols.warning)} Claude Code CLI not found`)
112
+ console.log(c.muted(` Install Claude Code for curation to work`))
113
+ allGood = false
114
+ }
115
+ } catch {
116
+ console.log(` ${c.warn(symbols.warning)} Claude Code CLI not found`)
117
+ console.log(c.muted(` Install Claude Code for curation to work`))
118
+ allGood = false
119
+ }
120
+ }
121
+
122
+ // 5. Check storage directory
123
+ const storageDir = join(homedir(), '.local', 'share', 'memory')
124
+ if (existsSync(storageDir)) {
125
+ console.log(` ${c.success(symbols.tick)} Storage directory exists`)
126
+ if (options.verbose) {
127
+ console.log(c.muted(` ${symbols.bullet} Path: ${storageDir}`))
128
+ }
129
+ } else {
130
+ console.log(` ${c.muted(symbols.info)} Storage directory will be created on first use`)
131
+ }
132
+
133
+ // Summary
134
+ console.log()
135
+ if (allGood) {
136
+ console.log(c.success(`${symbols.sparkles} All systems operational!`))
137
+ } else {
138
+ console.log(c.warn(`${symbols.warning} Some issues found - see above for details`))
139
+ }
140
+ console.log()
141
+
142
+ process.exit(allGood ? 0 : 1)
143
+ }