@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.
- package/dist/cli.js +93 -4
- package/dist/commands/add.js +25 -0
- package/dist/commands/delete.js +22 -0
- package/dist/commands/ingest.js +22 -9
- package/dist/commands/init.js +70 -23
- package/dist/commands/list.js +10 -0
- package/dist/commands/migrate.js +9 -8
- package/dist/commands/onboard.js +3 -3
- package/dist/commands/pull.js +2 -2
- package/dist/commands/setup.js +88 -4
- package/dist/commands/sync.js +206 -0
- package/dist/commands/tag.js +20 -0
- package/dist/commands/update.js +22 -0
- package/dist/commands/wizard.js +430 -0
- package/dist/hooks/hooks/session-end-check.sh +3 -3
- package/dist/hooks/hooks/strip-ai-coauthor.sh +22 -0
- package/dist/lib/notes.js +430 -18
- package/dist/mcp-server.js +33 -293
- package/package.json +1 -1
package/dist/commands/setup.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
|
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
|
+
}
|