@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.
- package/README.md +120 -0
- package/dist/cli.js +153 -0
- package/dist/commands/backup.js +89 -0
- package/dist/commands/check.js +79 -0
- package/dist/commands/config.js +115 -0
- package/dist/commands/export.js +21 -0
- package/dist/commands/ingest.js +221 -0
- package/dist/commands/init.js +142 -0
- package/dist/commands/onboard.js +190 -0
- package/dist/commands/pull.js +75 -0
- package/dist/commands/push.js +21 -0
- package/dist/commands/restore.js +72 -0
- package/dist/commands/setup.js +159 -0
- package/dist/commands/show.js +34 -0
- package/dist/hooks/block-env.sh +54 -0
- package/dist/hooks/hooks/block-env.sh +54 -0
- package/dist/hooks/hooks/post-write-ledger.sh +40 -0
- package/dist/hooks/hooks/session-end-check.sh +23 -0
- package/dist/hooks/post-write-ledger.sh +40 -0
- package/dist/hooks/session-end-check.sh +23 -0
- package/dist/lib/config.js +63 -0
- package/dist/lib/errors.js +23 -0
- package/dist/lib/generators.js +115 -0
- package/dist/lib/hash.js +4 -0
- package/dist/lib/migrate.js +23 -0
- package/dist/lib/notes.js +105 -0
- package/dist/lib/prompt.js +65 -0
- package/dist/mcp-server.js +348 -0
- package/dist/migrations/000-tracking.sql +4 -0
- package/dist/migrations/001-schema.sql +27 -0
- package/dist/migrations/002-functions.sql +14 -0
- package/dist/migrations/003-rls.sql +5 -0
- package/dist/migrations/migrations/000-tracking.sql +4 -0
- package/dist/migrations/migrations/001-schema.sql +27 -0
- package/dist/migrations/migrations/002-functions.sql +14 -0
- package/dist/migrations/migrations/003-rls.sql +5 -0
- 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
|
+
}
|