@aperdomoll90/ledger-ai 1.0.2 → 1.1.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.
@@ -1,10 +1,10 @@
1
- import { writeFileSync, readFileSync, copyFileSync, mkdirSync, existsSync, chmodSync } from 'fs';
1
+ import { writeFileSync, readFileSync, copyFileSync, mkdirSync, existsSync, chmodSync, unlinkSync } from 'fs';
2
2
  import { resolve, dirname } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
4
  import { execFileSync } from 'child_process';
5
5
  import { homedir } from 'os';
6
6
  import { loadConfig, loadConfigFile, getLedgerDir } from '../lib/config.js';
7
- import { fetchCachedNotes } from '../lib/notes.js';
7
+ import { fetchPersonaNotes } from '../lib/notes.js';
8
8
  import { generateClaudeMd } from '../lib/generators.js';
9
9
  import { ask } from '../lib/prompt.js';
10
10
  const __dirname = dirname(fileURLToPath(import.meta.url));
@@ -15,6 +15,90 @@ function verifyInit() {
15
15
  process.exit(1);
16
16
  }
17
17
  }
18
+ /** Detect whether a platform is currently installed/configured. */
19
+ export function detectPlatform(name) {
20
+ switch (name) {
21
+ case 'claude-code': {
22
+ try {
23
+ const output = execFileSync('claude', ['mcp', 'list'], { stdio: 'pipe', encoding: 'utf-8' });
24
+ const installed = output.toLowerCase().includes('ledger');
25
+ return { name, installed, detail: installed ? 'MCP registered' : 'Claude Code found, Ledger not registered' };
26
+ }
27
+ catch {
28
+ return { name, installed: false, detail: 'Claude Code CLI not found' };
29
+ }
30
+ }
31
+ case 'openclaw': {
32
+ // Check common locations for SOUL.md + USER.md
33
+ const configFile = loadConfigFile();
34
+ const openclawPath = configFile.openclawPath;
35
+ if (openclawPath && existsSync(resolve(openclawPath, 'SOUL.md')) && existsSync(resolve(openclawPath, 'USER.md'))) {
36
+ return { name, installed: true, detail: openclawPath };
37
+ }
38
+ return { name, installed: false };
39
+ }
40
+ case 'chatgpt': {
41
+ // ChatGPT has no persistent state — never detected as installed
42
+ return { name, installed: false, detail: 'No persistent state' };
43
+ }
44
+ }
45
+ }
46
+ // --- Uninstall functions ---
47
+ /** Remove Ledger MCP registration, hooks, and settings entries for Claude Code. */
48
+ export function uninstallClaudeCode() {
49
+ // 1. Remove MCP registration
50
+ try {
51
+ execFileSync('claude', ['mcp', 'remove', 'ledger', '-s', 'user'], { stdio: 'pipe' });
52
+ console.error(' Removed MCP registration.');
53
+ }
54
+ catch {
55
+ console.error(' MCP registration not found (already removed).');
56
+ }
57
+ // 2. Remove hook files
58
+ const claudeHooksDir = resolve(homedir(), '.claude/hooks');
59
+ const hookFiles = ['block-env.sh', 'post-write-ledger.sh', 'session-end-check.sh'];
60
+ for (const file of hookFiles) {
61
+ const hookPath = resolve(claudeHooksDir, file);
62
+ if (existsSync(hookPath)) {
63
+ unlinkSync(hookPath);
64
+ console.error(` Removed ${file}`);
65
+ }
66
+ }
67
+ // 3. Remove Ledger hook entries from settings.json
68
+ const settingsPath = resolve(homedir(), '.claude/settings.json');
69
+ if (existsSync(settingsPath)) {
70
+ try {
71
+ const settings = JSON.parse(readFileSync(settingsPath, 'utf-8'));
72
+ if (settings.hooks) {
73
+ delete settings.hooks;
74
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n');
75
+ console.error(' Removed hook entries from settings.json');
76
+ }
77
+ }
78
+ catch {
79
+ // Settings parse error — leave as-is
80
+ }
81
+ }
82
+ }
83
+ /** Remove SOUL.md and USER.md from OpenClaw workspace. */
84
+ export function uninstallOpenclaw(path) {
85
+ const configFile = loadConfigFile();
86
+ const targetPath = path || configFile.openclawPath;
87
+ if (!targetPath) {
88
+ console.error(' No OpenClaw workspace path configured.');
89
+ return;
90
+ }
91
+ const soulPath = resolve(targetPath, 'SOUL.md');
92
+ const userPath = resolve(targetPath, 'USER.md');
93
+ if (existsSync(soulPath)) {
94
+ unlinkSync(soulPath);
95
+ console.error(' Removed SOUL.md');
96
+ }
97
+ if (existsSync(userPath)) {
98
+ unlinkSync(userPath);
99
+ console.error(' Removed USER.md');
100
+ }
101
+ }
18
102
  export async function setupClaudeCode() {
19
103
  verifyInit();
20
104
  const config = loadConfig();
@@ -102,7 +186,7 @@ export async function setupOpenclaw(path) {
102
186
  process.exit(1);
103
187
  }
104
188
  console.error(`Setting up OpenClaw at ${targetPath}...\n`);
105
- const notes = await fetchCachedNotes(config.supabase);
189
+ const notes = await fetchPersonaNotes(config.supabase);
106
190
  const userNotes = notes.filter(n => n.metadata.type === 'user-preference');
107
191
  const feedbackNotes = notes.filter(n => n.metadata.type === 'feedback');
108
192
  // Generate SOUL.md (communication/behavior rules)
@@ -118,7 +202,7 @@ export async function setupOpenclaw(path) {
118
202
  export async function setupChatgpt() {
119
203
  verifyInit();
120
204
  const config = loadConfig();
121
- const notes = await fetchCachedNotes(config.supabase);
205
+ const notes = await fetchPersonaNotes(config.supabase);
122
206
  const feedbackNotes = notes.filter(n => n.metadata.type === 'feedback');
123
207
  const prompt = generateClaudeMd(feedbackNotes);
124
208
  console.error('WARNING: This is a snapshot, not a live connection.');
@@ -0,0 +1,206 @@
1
+ import { writeFileSync, readFileSync, mkdirSync, existsSync, unlinkSync, readdirSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { fetchPersonaNotes, updateNoteContent, updateNoteHash } from '../lib/notes.js';
4
+ import { contentHash } from '../lib/hash.js';
5
+ import { generateClaudeMd, generateMemoryMd } from '../lib/generators.js';
6
+ import { confirm } from '../lib/prompt.js';
7
+ export async function sync(config, options) {
8
+ const { quiet, force, dryRun } = options;
9
+ const notes = await fetchPersonaNotes(config.supabase);
10
+ const result = {
11
+ downloaded: [],
12
+ uploaded: [],
13
+ conflicts: [],
14
+ orphansRemoved: [],
15
+ skipped: [],
16
+ };
17
+ if (notes.length === 0) {
18
+ if (!quiet)
19
+ console.error('No persona notes found in Ledger.');
20
+ return result;
21
+ }
22
+ mkdirSync(config.memoryDir, { recursive: true });
23
+ const notesByFile = new Map();
24
+ for (const note of notes) {
25
+ const localFile = note.metadata.local_file;
26
+ if (localFile)
27
+ notesByFile.set(localFile, note);
28
+ }
29
+ // --- Phase 1: Process each persona note ---
30
+ for (const note of notes) {
31
+ const localFile = note.metadata.local_file;
32
+ if (!localFile)
33
+ continue;
34
+ const filePath = resolve(config.memoryDir, localFile);
35
+ const ledgerContent = note.content;
36
+ const ledgerHash = contentHash(ledgerContent);
37
+ const storedHash = note.metadata.content_hash;
38
+ if (!existsSync(filePath)) {
39
+ // File missing locally → download from Ledger
40
+ if (dryRun) {
41
+ if (!quiet)
42
+ console.error(` ${localFile} — would download (missing locally)`);
43
+ result.downloaded.push(localFile);
44
+ continue;
45
+ }
46
+ writeFileSync(filePath, `${ledgerContent}\n`, 'utf-8');
47
+ if (!storedHash || storedHash !== ledgerHash) {
48
+ await updateNoteHash(config.supabase, note.id, ledgerHash);
49
+ }
50
+ result.downloaded.push(localFile);
51
+ if (!quiet)
52
+ console.error(` ${localFile} — downloaded`);
53
+ continue;
54
+ }
55
+ // File exists locally — compare
56
+ const localRaw = readFileSync(filePath, 'utf-8').trim();
57
+ const localHash = contentHash(localRaw);
58
+ const localChanged = storedHash ? localHash !== storedHash : localHash !== ledgerHash;
59
+ const ledgerChanged = storedHash ? ledgerHash !== storedHash : false;
60
+ if (!localChanged && !ledgerChanged) {
61
+ // In sync — skip
62
+ result.skipped.push(localFile);
63
+ continue;
64
+ }
65
+ if (localChanged && !ledgerChanged) {
66
+ // Local modified, Ledger unchanged → push local to Ledger
67
+ if (dryRun) {
68
+ if (!quiet)
69
+ console.error(` ${localFile} — would upload (modified locally)`);
70
+ result.uploaded.push(localFile);
71
+ continue;
72
+ }
73
+ if (force) {
74
+ // --force means overwrite local with Ledger
75
+ writeFileSync(filePath, `${ledgerContent}\n`, 'utf-8');
76
+ result.downloaded.push(localFile);
77
+ if (!quiet)
78
+ console.error(` ${localFile} — overwritten with Ledger version (--force)`);
79
+ continue;
80
+ }
81
+ if (quiet) {
82
+ // In quiet mode (SessionStart hook), don't prompt — just flag it
83
+ console.log(`MODIFIED:${localFile}`);
84
+ result.conflicts.push(localFile);
85
+ continue;
86
+ }
87
+ // Interactive: ask user
88
+ console.error(`\n ${localFile} — modified locally`);
89
+ const shouldPush = await confirm(' Upload local changes to Ledger?');
90
+ if (shouldPush) {
91
+ await updateNoteContent(config.supabase, config.openai, note.id, localRaw);
92
+ await updateNoteHash(config.supabase, note.id, localHash);
93
+ result.uploaded.push(localFile);
94
+ console.error(` ${localFile} — uploaded to Ledger`);
95
+ }
96
+ else {
97
+ // Discard local, restore from Ledger
98
+ writeFileSync(filePath, `${ledgerContent}\n`, 'utf-8');
99
+ result.downloaded.push(localFile);
100
+ console.error(` ${localFile} — restored from Ledger`);
101
+ }
102
+ continue;
103
+ }
104
+ if (!localChanged && ledgerChanged) {
105
+ // Ledger updated, local unchanged → download
106
+ if (dryRun) {
107
+ if (!quiet)
108
+ console.error(` ${localFile} — would download (updated in Ledger)`);
109
+ result.downloaded.push(localFile);
110
+ continue;
111
+ }
112
+ writeFileSync(filePath, `${ledgerContent}\n`, 'utf-8');
113
+ await updateNoteHash(config.supabase, note.id, ledgerHash);
114
+ result.downloaded.push(localFile);
115
+ if (!quiet)
116
+ console.error(` ${localFile} — updated from Ledger`);
117
+ continue;
118
+ }
119
+ // Both changed — conflict
120
+ if (dryRun) {
121
+ if (!quiet)
122
+ console.error(` ${localFile} — CONFLICT (both changed)`);
123
+ result.conflicts.push(localFile);
124
+ continue;
125
+ }
126
+ if (force) {
127
+ writeFileSync(filePath, `${ledgerContent}\n`, 'utf-8');
128
+ result.downloaded.push(localFile);
129
+ if (!quiet)
130
+ console.error(` ${localFile} — overwritten with Ledger version (--force)`);
131
+ continue;
132
+ }
133
+ if (quiet) {
134
+ console.log(`CONFLICT:${localFile}`);
135
+ result.conflicts.push(localFile);
136
+ continue;
137
+ }
138
+ // Interactive: show conflict
139
+ console.error(`\n ${localFile} — CONFLICT (both changed)`);
140
+ const keepLedger = await confirm(' Keep Ledger version? (no = keep local and upload)');
141
+ if (keepLedger) {
142
+ writeFileSync(filePath, `${ledgerContent}\n`, 'utf-8');
143
+ await updateNoteHash(config.supabase, note.id, ledgerHash);
144
+ result.downloaded.push(localFile);
145
+ console.error(` ${localFile} — restored from Ledger`);
146
+ }
147
+ else {
148
+ await updateNoteContent(config.supabase, config.openai, note.id, localRaw);
149
+ await updateNoteHash(config.supabase, note.id, localHash);
150
+ result.uploaded.push(localFile);
151
+ console.error(` ${localFile} — uploaded to Ledger`);
152
+ }
153
+ }
154
+ // --- Phase 2: Detect orphaned local files (in memory/ but not in Ledger) ---
155
+ const localFiles = readdirSync(config.memoryDir)
156
+ .filter(f => f.endsWith('.md') && f !== 'MEMORY.md');
157
+ for (const file of localFiles) {
158
+ if (!notesByFile.has(file)) {
159
+ if (dryRun) {
160
+ if (!quiet)
161
+ console.error(` ${file} — orphaned (not in Ledger, would remove)`);
162
+ result.orphansRemoved.push(file);
163
+ continue;
164
+ }
165
+ // Orphaned cache file — Ledger note was deleted, remove local
166
+ const filePath = resolve(config.memoryDir, file);
167
+ unlinkSync(filePath);
168
+ result.orphansRemoved.push(file);
169
+ if (!quiet)
170
+ console.error(` ${file} — removed (no longer in Ledger)`);
171
+ }
172
+ }
173
+ // --- Phase 3: Regenerate MEMORY.md and CLAUDE.md ---
174
+ if (!dryRun) {
175
+ const allLocalFiles = [...result.downloaded, ...result.uploaded, ...result.skipped, ...result.conflicts];
176
+ const memoryPath = resolve(config.memoryDir, 'MEMORY.md');
177
+ writeFileSync(memoryPath, generateMemoryMd(allLocalFiles), 'utf-8');
178
+ const feedbackNotes = notes.filter(n => n.metadata.type === 'feedback');
179
+ const newClaudeMd = generateClaudeMd(feedbackNotes);
180
+ if (existsSync(config.claudeMdPath)) {
181
+ const existing = readFileSync(config.claudeMdPath, 'utf-8');
182
+ if (existing.startsWith('# Global Rules') || force) {
183
+ writeFileSync(config.claudeMdPath, newClaudeMd, 'utf-8');
184
+ if (!quiet)
185
+ console.error(' wrote ~/CLAUDE.md');
186
+ }
187
+ }
188
+ else {
189
+ writeFileSync(config.claudeMdPath, newClaudeMd, 'utf-8');
190
+ if (!quiet)
191
+ console.error(' wrote ~/CLAUDE.md');
192
+ }
193
+ }
194
+ // --- Summary ---
195
+ if (!quiet) {
196
+ const parts = [
197
+ result.downloaded.length > 0 ? `${result.downloaded.length} downloaded` : null,
198
+ result.uploaded.length > 0 ? `${result.uploaded.length} uploaded` : null,
199
+ result.conflicts.length > 0 ? `${result.conflicts.length} conflicts` : null,
200
+ result.orphansRemoved.length > 0 ? `${result.orphansRemoved.length} orphans removed` : null,
201
+ result.skipped.length > 0 ? `${result.skipped.length} in sync` : null,
202
+ ].filter(Boolean).join(', ');
203
+ console.error(`\nSync complete: ${parts || 'nothing to do'}`);
204
+ }
205
+ return result;
206
+ }
@@ -0,0 +1,20 @@
1
+ import { opUpdateMetadata } from '../lib/notes.js';
2
+ export async function tag(config, id, options) {
3
+ const metadata = {};
4
+ if (options.description)
5
+ metadata.description = options.description;
6
+ if (options.project)
7
+ metadata.project = options.project;
8
+ if (options.scope)
9
+ metadata.scope = options.scope;
10
+ if (Object.keys(metadata).length === 0) {
11
+ console.error('No metadata fields provided. Use --description, --project, or --scope.');
12
+ process.exit(1);
13
+ }
14
+ const result = await opUpdateMetadata({ supabase: config.supabase, openai: config.openai }, id, metadata);
15
+ if (result.status === 'error') {
16
+ console.error(result.message);
17
+ process.exit(1);
18
+ }
19
+ console.error(result.message);
20
+ }
@@ -0,0 +1,22 @@
1
+ import { opUpdateNote } from '../lib/notes.js';
2
+ import { confirm } from '../lib/prompt.js';
3
+ export async function update(config, id, content, options) {
4
+ const clients = { supabase: config.supabase, openai: config.openai };
5
+ // First call: show confirmation
6
+ const preview = await opUpdateNote(clients, id, content, options.metadata, false);
7
+ if (preview.status === 'error') {
8
+ console.error(preview.message);
9
+ process.exit(1);
10
+ }
11
+ console.error(preview.message);
12
+ const proceed = await confirm('\nProceed with update?');
13
+ if (!proceed) {
14
+ console.error('Cancelled.');
15
+ return;
16
+ }
17
+ // Second call: execute
18
+ const result = await opUpdateNote(clients, id, content, options.metadata, true);
19
+ console.error(result.message);
20
+ if (result.status === 'error')
21
+ process.exit(1);
22
+ }