1- // Global directory (~/.lore/) lifecycle: versioning, migrations, sidecar config.
1+ // Global directory (~/.lore/) lifecycle: setup, sidecar config, Redis config.
2+ // All setup is idempotent — safe to run on every session.
23
34const fs = require ( 'fs' ) ;
45const path = require ( 'path' ) ;
@@ -7,87 +8,106 @@ const { debug } = require('./debug');
78const { getGlobalPath } = require ( './config' ) ;
89
910const DEFAULT_SIDECAR_PORT = 9185 ;
11+ const DEFAULT_REDIS_PORT = 6379 ;
1012
11- /**
12- * Read globalStructureVersion from ~/.lore/config.json.
13- * Returns 0 if the file or field is missing.
14- */
15- function getGlobalStructureVersion ( ) {
16- try {
17- const configPath = path . join ( getGlobalPath ( ) , 'config.json' ) ;
18- if ( ! fs . existsSync ( configPath ) ) return 0 ;
19- const data = JSON . parse ( fs . readFileSync ( configPath , 'utf8' ) ) ;
20- return typeof data . globalStructureVersion === 'number' ? data . globalStructureVersion : 0 ;
21- } catch ( e ) {
22- debug ( 'getGlobalStructureVersion: %s' , e . message ) ;
23- return 0 ;
24- }
25- }
13+ const GLOBAL_DIRS = [
14+ 'AGENTIC/skills' ,
15+ 'AGENTIC/rules' ,
16+ 'AGENTIC/agents' ,
17+ 'knowledge-base/fieldnotes' ,
18+ 'knowledge-base/runbooks' ,
19+ 'knowledge-base/environment' ,
20+ 'knowledge-base/work-items' ,
21+ 'knowledge-base/drafts' ,
22+ 'redis-data' ,
23+ ] ;
2624
27- /**
28- * Return the highest version number from NNN-name.js migration files.
29- * Returns 0 if the directory is missing or empty.
30- */
31- function getRequiredStructureVersion ( migrationsDir ) {
32- try {
33- if ( ! fs . existsSync ( migrationsDir ) ) return 0 ;
34- const files = fs . readdirSync ( migrationsDir ) . filter ( f => / ^ \d { 3 } - .* \. j s $ / . test ( f ) ) . sort ( ) ;
35- if ( files . length === 0 ) return 0 ;
36- const last = files [ files . length - 1 ] ;
37- return parseInt ( last . slice ( 0 , 3 ) , 10 ) ;
38- } catch ( e ) {
39- debug ( 'getRequiredStructureVersion: %s' , e . message ) ;
40- return 0 ;
41- }
42- }
25+ const OPERATOR_PROFILE_CONTENT = `# Operator Profile
26+
27+ <!-- Injected into every session as OPERATOR PROFILE context. -->
28+ <!-- This file is gitignored — it stays local to your machine. -->
29+
30+ ## Identity
31+
32+ - **Name:**
33+ - **Role:**
34+
35+ ## Preferences
36+
37+ Add any preferences, working style notes, or context that should be
38+ available to the agent in every session.
39+ ` ;
40+
41+ const COMPOSE_CONTENT = `name: lore
42+ services:
43+ lore-runtime:
44+ image: lorehq/lore-memory:latest
45+ ports:
46+ - '9185:8080'
47+ environment:
48+ - REDIS_URL=redis://lore-memory:6379
49+ - DOCS_SOURCE=/data/knowledge-base
50+ - LORE_TOKEN=\${LORE_TOKEN}
51+ volumes:
52+ - ./knowledge-base:/data/knowledge-base:ro
53+ - runtime_data:/runtime-data
54+ depends_on:
55+ - lore-memory
56+ restart: unless-stopped
57+
58+ lore-memory:
59+ image: redis/redis-stack-server:latest
60+ ports:
61+ - '6379:6379'
62+ volumes:
63+ - ./redis-data:/data
64+ restart: unless-stopped
65+
66+ volumes:
67+ runtime_data:
68+ ` ;
4369
4470/**
45- * Apply pending migrations in order, bumping globalStructureVersion after each .
46- * Returns { ran: number, version: number } .
71+ * Ensure ~/.lore/ exists with the expected structure .
72+ * Fully idempotent — creates what's missing, never overwrites existing content .
4773 */
48- function runMigrations ( migrationsDir ) {
74+ function ensureGlobalDir ( ) {
4975 const globalPath = getGlobalPath ( ) ;
50- const current = getGlobalStructureVersion ( ) ;
51- let ran = 0 ;
5276
53- try {
54- const files = fs . readdirSync ( migrationsDir ) . filter ( f => / ^ \d { 3 } - .* \. j s $ / . test ( f ) ) . sort ( ) ;
55- for ( const file of files ) {
56- const version = parseInt ( file . slice ( 0 , 3 ) , 10 ) ;
57- if ( version <= current ) continue ;
58-
59- debug ( 'running migration %s' , file ) ;
60- const migration = require ( path . resolve ( migrationsDir , file ) ) ;
61- migration . up ( globalPath ) ;
62-
63- // Bump version after each migration (crash-safe: partial runs resume correctly)
64- const configPath = path . join ( globalPath , 'config.json' ) ;
65- let config = { } ;
66- try {
67- if ( fs . existsSync ( configPath ) ) {
68- config = JSON . parse ( fs . readFileSync ( configPath , 'utf8' ) ) ;
69- }
70- } catch { /* start fresh */ }
71- config . globalStructureVersion = version ;
72- fs . writeFileSync ( configPath , JSON . stringify ( config , null , 2 ) + '\n' ) ;
73- ran ++ ;
74- }
75- } catch ( e ) {
76- debug ( 'runMigrations: %s' , e . message ) ;
77- throw e ;
77+ // Directories
78+ for ( const dir of GLOBAL_DIRS ) {
79+ fs . mkdirSync ( path . join ( globalPath , dir ) , { recursive : true } ) ;
7880 }
7981
80- return { ran, version : getGlobalStructureVersion ( ) } ;
81- }
82+ // Operator profile — only if missing
83+ const profilePath = path . join ( globalPath , 'knowledge-base' , 'operator-profile.md' ) ;
84+ if ( ! fs . existsSync ( profilePath ) ) {
85+ fs . writeFileSync ( profilePath , OPERATOR_PROFILE_CONTENT ) ;
86+ }
8287
83- /**
84- * Ensure ~/.lore/ exists and run pending migrations.
85- * Returns { ran: number, version: number }.
86- */
87- function ensureGlobalDir ( migrationsDir ) {
88- const globalPath = getGlobalPath ( ) ;
89- fs . mkdirSync ( globalPath , { recursive : true } ) ;
90- return runMigrations ( migrationsDir ) ;
88+ // docker-compose.yml — create if missing, patch Redis port if existing
89+ const composePath = path . join ( globalPath , 'docker-compose.yml' ) ;
90+ if ( ! fs . existsSync ( composePath ) ) {
91+ fs . writeFileSync ( composePath , COMPOSE_CONTENT ) ;
92+ } else {
93+ let yml = fs . readFileSync ( composePath , 'utf8' ) ;
94+ if ( yml . includes ( 'lore-memory:' ) && ! yml . includes ( "'6379:6379'" ) ) {
95+ yml = yml . replace (
96+ / ( l o r e - m e m o r y : \n i m a g e : [ ^ \n ] + \n ) / ,
97+ '$1 ports:\n - \'6379:6379\'\n' ,
98+ ) ;
99+ fs . writeFileSync ( composePath , yml ) ;
100+ }
101+ }
102+
103+ // .env with LORE_TOKEN — only if missing or no token
104+ const envPath = path . join ( globalPath , '.env' ) ;
105+ let envContent = '' ;
106+ try { envContent = fs . readFileSync ( envPath , 'utf8' ) ; } catch { /* missing */ }
107+ if ( ! envContent . includes ( 'LORE_TOKEN=' ) ) {
108+ const token = crypto . randomBytes ( 32 ) . toString ( 'hex' ) ;
109+ fs . appendFileSync ( envPath , `LORE_TOKEN=${ token } \n` ) ;
110+ }
91111}
92112
93113/**
@@ -106,6 +126,22 @@ function getSidecarPort() {
106126 }
107127}
108128
129+ /**
130+ * Return the Redis port. Reads redisPort from ~/.lore/config.json,
131+ * falls back to 6379.
132+ */
133+ function getRedisPort ( ) {
134+ try {
135+ const configPath = path . join ( getGlobalPath ( ) , 'config.json' ) ;
136+ if ( ! fs . existsSync ( configPath ) ) return DEFAULT_REDIS_PORT ;
137+ const data = JSON . parse ( fs . readFileSync ( configPath , 'utf8' ) ) ;
138+ return data . redisPort || DEFAULT_REDIS_PORT ;
139+ } catch ( e ) {
140+ debug ( 'getRedisPort: %s' , e . message ) ;
141+ return DEFAULT_REDIS_PORT ;
142+ }
143+ }
144+
109145/**
110146 * Read LORE_TOKEN from ~/.lore/.env. Returns null if missing.
111147 */
@@ -134,6 +170,7 @@ function ensureGlobalToken() {
134170}
135171
136172module . exports = {
137- getGlobalStructureVersion, getRequiredStructureVersion, runMigrations, ensureGlobalDir,
138- getSidecarPort, getGlobalToken, ensureGlobalToken, DEFAULT_SIDECAR_PORT ,
173+ ensureGlobalDir,
174+ getSidecarPort, getRedisPort, getGlobalToken, ensureGlobalToken,
175+ DEFAULT_SIDECAR_PORT , DEFAULT_REDIS_PORT ,
139176} ;
0 commit comments