@aperdomoll90/ledger-ai 1.0.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.
Files changed (37) hide show
  1. package/README.md +120 -0
  2. package/dist/cli.js +153 -0
  3. package/dist/commands/backup.js +89 -0
  4. package/dist/commands/check.js +79 -0
  5. package/dist/commands/config.js +115 -0
  6. package/dist/commands/export.js +21 -0
  7. package/dist/commands/ingest.js +221 -0
  8. package/dist/commands/init.js +142 -0
  9. package/dist/commands/onboard.js +190 -0
  10. package/dist/commands/pull.js +75 -0
  11. package/dist/commands/push.js +21 -0
  12. package/dist/commands/restore.js +72 -0
  13. package/dist/commands/setup.js +159 -0
  14. package/dist/commands/show.js +34 -0
  15. package/dist/hooks/block-env.sh +54 -0
  16. package/dist/hooks/hooks/block-env.sh +54 -0
  17. package/dist/hooks/hooks/post-write-ledger.sh +40 -0
  18. package/dist/hooks/hooks/session-end-check.sh +23 -0
  19. package/dist/hooks/post-write-ledger.sh +40 -0
  20. package/dist/hooks/session-end-check.sh +23 -0
  21. package/dist/lib/config.js +63 -0
  22. package/dist/lib/errors.js +23 -0
  23. package/dist/lib/generators.js +115 -0
  24. package/dist/lib/hash.js +4 -0
  25. package/dist/lib/migrate.js +23 -0
  26. package/dist/lib/notes.js +105 -0
  27. package/dist/lib/prompt.js +65 -0
  28. package/dist/mcp-server.js +348 -0
  29. package/dist/migrations/000-tracking.sql +4 -0
  30. package/dist/migrations/001-schema.sql +27 -0
  31. package/dist/migrations/002-functions.sql +14 -0
  32. package/dist/migrations/003-rls.sql +5 -0
  33. package/dist/migrations/migrations/000-tracking.sql +4 -0
  34. package/dist/migrations/migrations/001-schema.sql +27 -0
  35. package/dist/migrations/migrations/002-functions.sql +14 -0
  36. package/dist/migrations/migrations/003-rls.sql +5 -0
  37. package/package.json +60 -0
@@ -0,0 +1,72 @@
1
+ import { readFileSync, existsSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { confirm } from '../lib/prompt.js';
4
+ export async function restore(config, filePath) {
5
+ const absPath = resolve(filePath);
6
+ if (!existsSync(absPath)) {
7
+ console.error(`File not found: ${absPath}`);
8
+ process.exit(1);
9
+ }
10
+ let notes;
11
+ try {
12
+ notes = JSON.parse(readFileSync(absPath, 'utf-8'));
13
+ }
14
+ catch {
15
+ console.error('Invalid JSON file.');
16
+ process.exit(1);
17
+ }
18
+ console.error(`Found ${notes.length} notes in backup.`);
19
+ // Check current database
20
+ const { count } = await config.supabase
21
+ .from('notes')
22
+ .select('*', { count: 'exact', head: true });
23
+ if (count && count > 0) {
24
+ console.error(`Database already has ${count} notes.`);
25
+ const proceed = await confirm('Restore will add notes (not replace). Continue?');
26
+ if (!proceed) {
27
+ console.error('Cancelled.');
28
+ return;
29
+ }
30
+ }
31
+ console.error('Restoring...\n');
32
+ let restored = 0;
33
+ let skipped = 0;
34
+ for (const note of notes) {
35
+ // Check for existing note with same upsert_key
36
+ const upsertKey = note.metadata.upsert_key;
37
+ if (upsertKey) {
38
+ const { data: existing } = await config.supabase
39
+ .from('notes')
40
+ .select('id')
41
+ .eq('metadata->>upsert_key', upsertKey)
42
+ .limit(1)
43
+ .single();
44
+ if (existing) {
45
+ console.error(` skip "${upsertKey}" (already exists)`);
46
+ skipped++;
47
+ continue;
48
+ }
49
+ }
50
+ // Generate embedding
51
+ const embeddingResponse = await config.openai.embeddings.create({
52
+ model: 'text-embedding-3-small',
53
+ input: note.content,
54
+ });
55
+ const embedding = embeddingResponse.data[0].embedding;
56
+ const { error } = await config.supabase
57
+ .from('notes')
58
+ .insert({
59
+ content: note.content,
60
+ metadata: note.metadata,
61
+ embedding,
62
+ });
63
+ if (error) {
64
+ console.error(` error restoring note ${note.id}: ${error.message}`);
65
+ continue;
66
+ }
67
+ const label = upsertKey || `note-${note.id}`;
68
+ console.error(` restored "${label}"`);
69
+ restored++;
70
+ }
71
+ console.error(`\nRestore complete: ${restored} restored, ${skipped} skipped (already exist)`);
72
+ }
@@ -0,0 +1,159 @@
1
+ import { writeFileSync, readFileSync, copyFileSync, mkdirSync, existsSync, chmodSync } from 'fs';
2
+ import { resolve, dirname } from 'path';
3
+ import { fileURLToPath } from 'url';
4
+ import { execFileSync } from 'child_process';
5
+ import { homedir } from 'os';
6
+ import { loadConfig, loadConfigFile, getLedgerDir } from '../lib/config.js';
7
+ import { fetchCachedNotes } from '../lib/notes.js';
8
+ import { generateClaudeMd } from '../lib/generators.js';
9
+ import { ask } from '../lib/prompt.js';
10
+ const __dirname = dirname(fileURLToPath(import.meta.url));
11
+ function verifyInit() {
12
+ const envPath = resolve(getLedgerDir(), '.env');
13
+ if (!existsSync(envPath)) {
14
+ console.error('Ledger not initialized. Run `ledger init` first.');
15
+ process.exit(1);
16
+ }
17
+ }
18
+ export async function setupClaudeCode() {
19
+ verifyInit();
20
+ const config = loadConfig();
21
+ const configFile = loadConfigFile();
22
+ const hooks = configFile.hooks || {};
23
+ console.error('Setting up Claude Code...\n');
24
+ // 1. Register MCP server
25
+ const mcpServerPath = resolve(__dirname, '../mcp-server.js');
26
+ const envPath = resolve(getLedgerDir(), '.env');
27
+ console.error('Registering MCP server...');
28
+ try {
29
+ // Remove existing registration first (idempotent)
30
+ try {
31
+ execFileSync('claude', ['mcp', 'remove', 'ledger', '-s', 'user'], { stdio: 'pipe' });
32
+ }
33
+ catch {
34
+ // Not registered yet — fine
35
+ }
36
+ execFileSync('claude', [
37
+ 'mcp', 'add', '-s', 'user',
38
+ '-e', `DOTENV_CONFIG_PATH=${envPath}`,
39
+ '--', 'ledger', 'node', mcpServerPath,
40
+ ], { stdio: 'pipe' });
41
+ console.error(' MCP server registered.\n');
42
+ }
43
+ catch {
44
+ console.error(' Failed to register MCP. Is Claude Code installed?');
45
+ console.error(' Install: npm install -g @anthropic-ai/claude-code');
46
+ process.exit(1);
47
+ }
48
+ // 2. Install hooks
49
+ const claudeHooksDir = resolve(homedir(), '.claude/hooks');
50
+ mkdirSync(claudeHooksDir, { recursive: true });
51
+ const hooksSourceDir = resolve(__dirname, '../hooks');
52
+ const hookFiles = [
53
+ { src: 'block-env.sh', enabled: hooks.envBlocking !== false },
54
+ { src: 'post-write-ledger.sh', enabled: hooks.writeInterception !== false },
55
+ { src: 'session-end-check.sh', enabled: hooks.sessionEndCheck !== false },
56
+ ];
57
+ console.error('Installing hooks...');
58
+ for (const hook of hookFiles) {
59
+ if (!hook.enabled) {
60
+ console.error(` skip ${hook.src} (disabled in config)`);
61
+ continue;
62
+ }
63
+ const dest = resolve(claudeHooksDir, hook.src);
64
+ copyFileSync(resolve(hooksSourceDir, hook.src), dest);
65
+ chmodSync(dest, 0o755);
66
+ console.error(` installed ${hook.src}`);
67
+ }
68
+ // 3. Update settings.json
69
+ const settingsPath = resolve(homedir(), '.claude/settings.json');
70
+ let settings = {};
71
+ if (existsSync(settingsPath)) {
72
+ try {
73
+ settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
74
+ }
75
+ catch {
76
+ settings = {};
77
+ }
78
+ }
79
+ settings.hooks = buildHookSettings(hooks);
80
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
81
+ console.error(' Updated ~/.claude/settings.json\n');
82
+ // 4. Pull
83
+ console.error('Pulling notes from Ledger...');
84
+ try {
85
+ execFileSync('ledger', ['pull', '--force'], { stdio: 'inherit' });
86
+ }
87
+ catch {
88
+ console.error(' Pull failed. You can run `ledger pull --force` manually.');
89
+ }
90
+ console.error('\nClaude Code is ready. Start a new session.');
91
+ }
92
+ export async function setupOpenclaw(path) {
93
+ verifyInit();
94
+ const config = loadConfig();
95
+ let targetPath = path;
96
+ if (!targetPath) {
97
+ targetPath = await ask('Where is your OpenClaw workspace? ');
98
+ }
99
+ targetPath = resolve(targetPath);
100
+ if (!existsSync(targetPath)) {
101
+ console.error(`Directory not found: ${targetPath}`);
102
+ process.exit(1);
103
+ }
104
+ console.error(`Setting up OpenClaw at ${targetPath}...\n`);
105
+ const notes = await fetchCachedNotes(config.supabase);
106
+ const userNotes = notes.filter(n => n.metadata.type === 'user-preference');
107
+ const feedbackNotes = notes.filter(n => n.metadata.type === 'feedback');
108
+ // Generate SOUL.md (communication/behavior rules)
109
+ const soulContent = feedbackNotes.map(n => n.content).join('\n\n---\n\n');
110
+ writeFileSync(resolve(targetPath, 'SOUL.md'), soulContent + '\n');
111
+ console.error(' wrote SOUL.md');
112
+ // Generate USER.md (user profile)
113
+ const userContent = userNotes.map(n => n.content).join('\n\n---\n\n');
114
+ writeFileSync(resolve(targetPath, 'USER.md'), userContent + '\n');
115
+ console.error(' wrote USER.md');
116
+ console.error(`\nOpenClaw persona written. Sync via \`ledger\` CLI.`);
117
+ }
118
+ export async function setupChatgpt() {
119
+ verifyInit();
120
+ const config = loadConfig();
121
+ const notes = await fetchCachedNotes(config.supabase);
122
+ const feedbackNotes = notes.filter(n => n.metadata.type === 'feedback');
123
+ const prompt = generateClaudeMd(feedbackNotes);
124
+ console.error('WARNING: This is a snapshot, not a live connection.');
125
+ console.error('Run `ledger setup chatgpt` again to regenerate after changes.\n');
126
+ console.error('Copy the text below into ChatGPT > Settings > Custom Instructions:\n');
127
+ console.error('---\n');
128
+ console.log(prompt);
129
+ console.error('---');
130
+ }
131
+ function buildHookSettings(hooks) {
132
+ const result = {
133
+ SessionStart: [
134
+ {
135
+ matcher: '',
136
+ hooks: [{ type: 'command', command: 'ledger pull --quiet' }],
137
+ },
138
+ ],
139
+ };
140
+ const preToolUse = [];
141
+ const postToolUse = [];
142
+ const stop = [];
143
+ if (hooks.envBlocking !== false || hooks.mcpJsonBlocking !== false) {
144
+ preToolUse.push({ matcher: 'Read', hooks: [{ type: 'command', command: '~/.claude/hooks/block-env.sh', timeout: 5 }] }, { matcher: 'Edit|Write', hooks: [{ type: 'command', command: '~/.claude/hooks/block-env.sh', timeout: 5 }] });
145
+ }
146
+ if (hooks.writeInterception !== false) {
147
+ postToolUse.push({ matcher: 'Edit|Write', hooks: [{ type: 'command', command: '~/.claude/hooks/post-write-ledger.sh', timeout: 10 }] });
148
+ }
149
+ if (hooks.sessionEndCheck !== false) {
150
+ stop.push({ matcher: '', hooks: [{ type: 'command', command: '~/.claude/hooks/session-end-check.sh', timeout: 15 }] });
151
+ }
152
+ if (preToolUse.length > 0)
153
+ result.PreToolUse = preToolUse;
154
+ if (postToolUse.length > 0)
155
+ result.PostToolUse = postToolUse;
156
+ if (stop.length > 0)
157
+ result.Stop = stop;
158
+ return result;
159
+ }
@@ -0,0 +1,34 @@
1
+ import { writeFileSync, mkdirSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { execFileSync } from 'child_process';
4
+ import { searchNotes } from '../lib/notes.js';
5
+ import { fatal, ExitCode } from '../lib/errors.js';
6
+ const VIEW_DIR = '/tmp/ledger-view';
7
+ export async function show(config, query, options = {}) {
8
+ // Fetch more if filtering
9
+ const fetchLimit = (options.type || options.project) ? 10 : 1;
10
+ let results = await searchNotes(config.supabase, config.openai, query, 0.3, fetchLimit);
11
+ if (options.type) {
12
+ results = results.filter(n => n.metadata.type === options.type);
13
+ }
14
+ if (options.project) {
15
+ results = results.filter(n => n.metadata.project === options.project);
16
+ }
17
+ if (results.length === 0) {
18
+ fatal('No matching notes found.', ExitCode.NOTE_NOT_FOUND);
19
+ }
20
+ const note = results[0];
21
+ const upsertKey = note.metadata.upsert_key || `note-${note.id}`;
22
+ const filename = `${upsertKey}.md`;
23
+ mkdirSync(VIEW_DIR, { recursive: true });
24
+ const filePath = resolve(VIEW_DIR, filename);
25
+ writeFileSync(filePath, note.content + '\n', 'utf-8');
26
+ console.log(`Match: "${upsertKey}" (similarity: ${note.similarity.toFixed(3)})`);
27
+ console.log(filePath);
28
+ try {
29
+ execFileSync('code', [filePath], { stdio: 'ignore' });
30
+ }
31
+ catch {
32
+ // VS Code not available — path already printed
33
+ }
34
+ }
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Block reading or writing sensitive files
3
+ INPUT=$(cat)
4
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
5
+
6
+ if [[ -z "$FILE_PATH" ]]; then
7
+ exit 0
8
+ fi
9
+
10
+ FILENAME=$(basename "$FILE_PATH")
11
+
12
+ # Block .env files (all variants)
13
+ if [[ "$FILENAME" =~ ^\.env($|\.) ]]; then
14
+ echo "BLOCKED: .env files must never be read or written. Check existence with 'test -f .env' or 'wc -l .env'." >&2
15
+ exit 2
16
+ fi
17
+
18
+ # Block credential/secret files
19
+ case "$FILENAME" in
20
+ credentials.json|service-account.json|token.json|secrets.json|auth.json)
21
+ echo "BLOCKED: $FILENAME contains credentials. Do not read or write directly." >&2
22
+ exit 2
23
+ ;;
24
+ .npmrc|.netrc)
25
+ echo "BLOCKED: $FILENAME may contain auth tokens. Do not read or write directly." >&2
26
+ exit 2
27
+ ;;
28
+ mcp.json)
29
+ echo "BLOCKED: Do not edit mcp.json directly. Use 'claude mcp add -s user <name> -- <command>' instead." >&2
30
+ exit 2
31
+ ;;
32
+ esac
33
+
34
+ # Block by extension (keys, certs)
35
+ case "$FILENAME" in
36
+ *.pem|*.key|*.p12|*.pfx)
37
+ echo "BLOCKED: $FILENAME is a key/certificate file. Do not read or write." >&2
38
+ exit 2
39
+ ;;
40
+ esac
41
+
42
+ # Block SSH keys
43
+ if [[ "$FILE_PATH" =~ /.ssh/ ]] && [[ "$FILENAME" == id_* || "$FILENAME" == *.pub ]]; then
44
+ echo "BLOCKED: SSH keys must never be read. Check existence with 'test -f'." >&2
45
+ exit 2
46
+ fi
47
+
48
+ # Block AWS credentials
49
+ if [[ "$FILE_PATH" =~ /.aws/credentials ]]; then
50
+ echo "BLOCKED: AWS credentials must never be read directly." >&2
51
+ exit 2
52
+ fi
53
+
54
+ exit 0
@@ -0,0 +1,54 @@
1
+ #!/bin/bash
2
+ # Block reading or writing sensitive files
3
+ INPUT=$(cat)
4
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
5
+
6
+ if [[ -z "$FILE_PATH" ]]; then
7
+ exit 0
8
+ fi
9
+
10
+ FILENAME=$(basename "$FILE_PATH")
11
+
12
+ # Block .env files (all variants)
13
+ if [[ "$FILENAME" =~ ^\.env($|\.) ]]; then
14
+ echo "BLOCKED: .env files must never be read or written. Check existence with 'test -f .env' or 'wc -l .env'." >&2
15
+ exit 2
16
+ fi
17
+
18
+ # Block credential/secret files
19
+ case "$FILENAME" in
20
+ credentials.json|service-account.json|token.json|secrets.json|auth.json)
21
+ echo "BLOCKED: $FILENAME contains credentials. Do not read or write directly." >&2
22
+ exit 2
23
+ ;;
24
+ .npmrc|.netrc)
25
+ echo "BLOCKED: $FILENAME may contain auth tokens. Do not read or write directly." >&2
26
+ exit 2
27
+ ;;
28
+ mcp.json)
29
+ echo "BLOCKED: Do not edit mcp.json directly. Use 'claude mcp add -s user <name> -- <command>' instead." >&2
30
+ exit 2
31
+ ;;
32
+ esac
33
+
34
+ # Block by extension (keys, certs)
35
+ case "$FILENAME" in
36
+ *.pem|*.key|*.p12|*.pfx)
37
+ echo "BLOCKED: $FILENAME is a key/certificate file. Do not read or write." >&2
38
+ exit 2
39
+ ;;
40
+ esac
41
+
42
+ # Block SSH keys
43
+ if [[ "$FILE_PATH" =~ /.ssh/ ]] && [[ "$FILENAME" == id_* || "$FILENAME" == *.pub ]]; then
44
+ echo "BLOCKED: SSH keys must never be read. Check existence with 'test -f'." >&2
45
+ exit 2
46
+ fi
47
+
48
+ # Block AWS credentials
49
+ if [[ "$FILE_PATH" =~ /.aws/credentials ]]; then
50
+ echo "BLOCKED: AWS credentials must never be read directly." >&2
51
+ exit 2
52
+ fi
53
+
54
+ exit 0
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook for Write/Edit
3
+ # If a .md file is written to the memory directory, auto-ingest it to Ledger.
4
+ HOME_PROJECT=$(echo "$HOME" | sed 's|/|-|g')
5
+ MEMORY_DIR="$HOME/.claude/projects/$HOME_PROJECT/memory"
6
+ INPUT=$(cat)
7
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
8
+
9
+ if [[ -z "$FILE_PATH" ]]; then
10
+ exit 0
11
+ fi
12
+
13
+ # Check if it's a ledger-view temp file — auto-push back to Ledger
14
+ VIEW_DIR="/tmp/ledger-view"
15
+ if [[ "$FILE_PATH" == "$VIEW_DIR/"* ]]; then
16
+ ledger push "$FILE_PATH" 2>&1
17
+ exit 0
18
+ fi
19
+
20
+ # Only intercept writes to memory directory
21
+ if [[ "$FILE_PATH" != "$MEMORY_DIR/"* ]]; then
22
+ exit 0
23
+ fi
24
+
25
+ FILENAME=$(basename "$FILE_PATH")
26
+
27
+ # Skip generated files
28
+ if [[ "$FILENAME" == "MEMORY.md" ]]; then
29
+ exit 0
30
+ fi
31
+
32
+ # Only .md files
33
+ if [[ "$FILENAME" != *.md ]]; then
34
+ exit 0
35
+ fi
36
+
37
+ # Auto-ingest: send to Ledger, delete local
38
+ ledger ingest "$FILE_PATH" --auto 2>&1
39
+
40
+ exit 0
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+ # Session end: run hash-based sync check + temp file alert
3
+
4
+ OUTPUT=$(ledger check 2>/dev/null)
5
+
6
+ # Only show output if there are issues (not "All synced.")
7
+ if ! echo "$OUTPUT" | grep -q "All synced"; then
8
+ echo "$OUTPUT"
9
+ fi
10
+
11
+ # Check for leftover temp view files
12
+ VIEW_DIR="/tmp/ledger-view"
13
+ if [[ -d "$VIEW_DIR" ]]; then
14
+ COUNT=$(find "$VIEW_DIR" -name "*.md" 2>/dev/null | wc -l)
15
+ if [[ "$COUNT" -gt 0 ]]; then
16
+ echo "TEMP_VIEW_FILES:$COUNT files in /tmp/ledger-view/"
17
+ ls "$VIEW_DIR"/*.md 2>/dev/null | while read f; do
18
+ echo " $(basename "$f")"
19
+ done
20
+ fi
21
+ fi
22
+
23
+ exit 0
@@ -0,0 +1,40 @@
1
+ #!/bin/bash
2
+ # PostToolUse hook for Write/Edit
3
+ # If a .md file is written to the memory directory, auto-ingest it to Ledger.
4
+ HOME_PROJECT=$(echo "$HOME" | sed 's|/|-|g')
5
+ MEMORY_DIR="$HOME/.claude/projects/$HOME_PROJECT/memory"
6
+ INPUT=$(cat)
7
+ FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
8
+
9
+ if [[ -z "$FILE_PATH" ]]; then
10
+ exit 0
11
+ fi
12
+
13
+ # Check if it's a ledger-view temp file — auto-push back to Ledger
14
+ VIEW_DIR="/tmp/ledger-view"
15
+ if [[ "$FILE_PATH" == "$VIEW_DIR/"* ]]; then
16
+ ledger push "$FILE_PATH" 2>&1
17
+ exit 0
18
+ fi
19
+
20
+ # Only intercept writes to memory directory
21
+ if [[ "$FILE_PATH" != "$MEMORY_DIR/"* ]]; then
22
+ exit 0
23
+ fi
24
+
25
+ FILENAME=$(basename "$FILE_PATH")
26
+
27
+ # Skip generated files
28
+ if [[ "$FILENAME" == "MEMORY.md" ]]; then
29
+ exit 0
30
+ fi
31
+
32
+ # Only .md files
33
+ if [[ "$FILENAME" != *.md ]]; then
34
+ exit 0
35
+ fi
36
+
37
+ # Auto-ingest: send to Ledger, delete local
38
+ ledger ingest "$FILE_PATH" --auto 2>&1
39
+
40
+ exit 0
@@ -0,0 +1,23 @@
1
+ #!/bin/bash
2
+ # Session end: run hash-based sync check + temp file alert
3
+
4
+ OUTPUT=$(ledger check 2>/dev/null)
5
+
6
+ # Only show output if there are issues (not "All synced.")
7
+ if ! echo "$OUTPUT" | grep -q "All synced"; then
8
+ echo "$OUTPUT"
9
+ fi
10
+
11
+ # Check for leftover temp view files
12
+ VIEW_DIR="/tmp/ledger-view"
13
+ if [[ -d "$VIEW_DIR" ]]; then
14
+ COUNT=$(find "$VIEW_DIR" -name "*.md" 2>/dev/null | wc -l)
15
+ if [[ "$COUNT" -gt 0 ]]; then
16
+ echo "TEMP_VIEW_FILES:$COUNT files in /tmp/ledger-view/"
17
+ ls "$VIEW_DIR"/*.md 2>/dev/null | while read f; do
18
+ echo " $(basename "$f")"
19
+ done
20
+ fi
21
+ fi
22
+
23
+ exit 0
@@ -0,0 +1,63 @@
1
+ import dotenv from 'dotenv';
2
+ import { createClient } from '@supabase/supabase-js';
3
+ import OpenAI from 'openai';
4
+ import { resolve } from 'path';
5
+ import { homedir } from 'os';
6
+ import { existsSync, readFileSync } from 'fs';
7
+ import { fatal, ExitCode } from './errors.js';
8
+ // --- Defaults ---
9
+ const LEDGER_DIR = resolve(homedir(), '.ledger');
10
+ const LEDGER_DOTENV = resolve(LEDGER_DIR, '.env');
11
+ // Claude Code encodes the working directory as folder name: /home/user → -home-user
12
+ const HOME_PROJECT_DIR = homedir().replace(/\//g, '-');
13
+ const DEFAULT_MEMORY_DIR = resolve(homedir(), `.claude/projects/${HOME_PROJECT_DIR}/memory`);
14
+ const DEFAULT_CLAUDE_MD_PATH = resolve(homedir(), 'CLAUDE.md');
15
+ const CONFIG_FILE = resolve(LEDGER_DIR, 'config.json');
16
+ // --- Helpers ---
17
+ export function getLedgerDir() {
18
+ return LEDGER_DIR;
19
+ }
20
+ export function loadConfigFile() {
21
+ if (existsSync(CONFIG_FILE)) {
22
+ try {
23
+ return JSON.parse(readFileSync(CONFIG_FILE, 'utf-8'));
24
+ }
25
+ catch {
26
+ return {};
27
+ }
28
+ }
29
+ return {};
30
+ }
31
+ export function getDefaultConfig() {
32
+ return {
33
+ memoryDir: DEFAULT_MEMORY_DIR,
34
+ claudeMdPath: DEFAULT_CLAUDE_MD_PATH,
35
+ hooks: {
36
+ envBlocking: true,
37
+ mcpJsonBlocking: true,
38
+ writeInterception: true,
39
+ sessionEndCheck: true,
40
+ },
41
+ };
42
+ }
43
+ // --- Load Config ---
44
+ export function loadConfig() {
45
+ // Priority: env vars > DOTENV_CONFIG_PATH > ~/.ledger/.env
46
+ const dotenvPath = process.env.DOTENV_CONFIG_PATH
47
+ || (existsSync(LEDGER_DOTENV) ? LEDGER_DOTENV : undefined);
48
+ if (dotenvPath)
49
+ dotenv.config({ path: dotenvPath, quiet: true });
50
+ if (!process.env.SUPABASE_URL || !process.env.SUPABASE_SERVICE_ROLE_KEY) {
51
+ fatal('Missing SUPABASE_URL or SUPABASE_SERVICE_ROLE_KEY. Run `ledger init` or check your .env file.', ExitCode.GENERAL_ERROR);
52
+ }
53
+ if (!process.env.OPENAI_API_KEY) {
54
+ fatal('Missing OPENAI_API_KEY. Run `ledger init` or check your .env file.', ExitCode.GENERAL_ERROR);
55
+ }
56
+ const fileConfig = loadConfigFile();
57
+ return {
58
+ memoryDir: process.env.LEDGER_MEMORY_DIR || fileConfig.memoryDir || DEFAULT_MEMORY_DIR,
59
+ claudeMdPath: process.env.LEDGER_CLAUDE_MD_PATH || fileConfig.claudeMdPath || DEFAULT_CLAUDE_MD_PATH,
60
+ supabase: createClient(process.env.SUPABASE_URL, process.env.SUPABASE_SERVICE_ROLE_KEY),
61
+ openai: new OpenAI({ apiKey: process.env.OPENAI_API_KEY }),
62
+ };
63
+ }
@@ -0,0 +1,23 @@
1
+ export class LedgerError extends Error {
2
+ code;
3
+ constructor(message, code) {
4
+ super(message);
5
+ this.code = code;
6
+ this.name = 'LedgerError';
7
+ }
8
+ }
9
+ export var ExitCode;
10
+ (function (ExitCode) {
11
+ ExitCode[ExitCode["SUCCESS"] = 0] = "SUCCESS";
12
+ ExitCode[ExitCode["GENERAL_ERROR"] = 1] = "GENERAL_ERROR";
13
+ ExitCode[ExitCode["FILE_NOT_FOUND"] = 2] = "FILE_NOT_FOUND";
14
+ ExitCode[ExitCode["NOTE_NOT_FOUND"] = 3] = "NOTE_NOT_FOUND";
15
+ ExitCode[ExitCode["SUPABASE_ERROR"] = 4] = "SUPABASE_ERROR";
16
+ ExitCode[ExitCode["EMBEDDING_ERROR"] = 5] = "EMBEDDING_ERROR";
17
+ ExitCode[ExitCode["CONFLICT"] = 6] = "CONFLICT";
18
+ ExitCode[ExitCode["INVALID_INPUT"] = 7] = "INVALID_INPUT";
19
+ })(ExitCode || (ExitCode = {}));
20
+ export function fatal(message, code = ExitCode.GENERAL_ERROR) {
21
+ console.error(message);
22
+ process.exit(code);
23
+ }