@aperdomoll90/ledger-ai 1.0.1 → 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.
@@ -0,0 +1,430 @@
1
+ import { writeFileSync } from 'fs';
2
+ import { resolve } from 'path';
3
+ import { homedir } from 'os';
4
+ import { createClient } from '@supabase/supabase-js';
5
+ import OpenAI from 'openai';
6
+ import { ask, confirm, choose } from '../lib/prompt.js';
7
+ import { getLedgerDir, loadConfigFile } from '../lib/config.js';
8
+ import { fetchPersonaNotes } from '../lib/notes.js';
9
+ import { gatherCredentials, connectAndMigrate, hasCredentials, readCredentials } from './init.js';
10
+ import { onboard } from './onboard.js';
11
+ import { setupClaudeCode, setupOpenclaw, setupChatgpt, detectPlatform, uninstallClaudeCode, uninstallOpenclaw, } from './setup.js';
12
+ import { sync } from './sync.js';
13
+ import { getMemoryFiles } from './migrate.js';
14
+ function ok() { return { ran: true, skipped: false }; }
15
+ function skipped() { return { ran: false, skipped: true }; }
16
+ function failed(msg) { return { ran: true, skipped: false, error: msg }; }
17
+ // --- Wizard ---
18
+ export async function wizard() {
19
+ console.error('Ledger Init Wizard\n');
20
+ // Detect what's already done
21
+ const checks = detectAllSteps();
22
+ if (checks.allDone) {
23
+ await showAlreadySetUp(checks);
24
+ return;
25
+ }
26
+ // Run steps sequentially
27
+ let creds = null;
28
+ let connectResult = null;
29
+ let config = null;
30
+ // Step 1: Credentials
31
+ if (checks.credentials) {
32
+ console.error('Step 1: Credentials: found (Supabase + OpenAI)\n');
33
+ creds = readCredentials();
34
+ }
35
+ else {
36
+ console.error('Step 1: Credentials\n');
37
+ const result = await runNonSkippable('Credentials', async () => {
38
+ creds = await gatherCredentials();
39
+ });
40
+ if (result.error)
41
+ return;
42
+ }
43
+ // Step 2: Database
44
+ if (!creds) {
45
+ creds = readCredentials();
46
+ if (!creds) {
47
+ console.error('Cannot proceed without credentials.');
48
+ return;
49
+ }
50
+ }
51
+ if (checks.database) {
52
+ console.error(`Step 2: Database: connected (${checks.noteCount} notes)\n`);
53
+ // Construct clients from existing creds
54
+ const supabase = createClient(creds.supabaseUrl, creds.supabaseKey);
55
+ const openai = new OpenAI({ apiKey: creds.openaiKey });
56
+ connectResult = { supabase, openai, noteCount: checks.noteCount };
57
+ }
58
+ else {
59
+ console.error('Step 2: Connect to database\n');
60
+ const result = await runNonSkippable('Database', async () => {
61
+ connectResult = await connectAndMigrate(creds);
62
+ });
63
+ if (result.error)
64
+ return;
65
+ }
66
+ // Build LedgerConfig for remaining steps
67
+ if (connectResult) {
68
+ const configFile = loadConfigFile();
69
+ const HOME_PROJECT_DIR = homedir().replace(/\//g, '-');
70
+ config = {
71
+ memoryDir: configFile.memoryDir || resolve(homedir(), `.claude/projects/${HOME_PROJECT_DIR}/memory`),
72
+ claudeMdPath: configFile.claudeMdPath || resolve(homedir(), 'CLAUDE.md'),
73
+ supabase: connectResult.supabase,
74
+ openai: connectResult.openai,
75
+ };
76
+ }
77
+ if (!config) {
78
+ console.error('Cannot proceed without database connection.');
79
+ return;
80
+ }
81
+ // Step 3: Device alias
82
+ if (checks.device) {
83
+ console.error(`Step 3: Device: ${checks.deviceAlias}\n`);
84
+ }
85
+ else {
86
+ console.error('Step 3: Device alias (optional)\n');
87
+ await runSkippable('Device alias', () => stepDeviceAlias(config));
88
+ }
89
+ // Step 4: Persona
90
+ if (checks.persona) {
91
+ console.error(`Step 4: Persona: found\n`);
92
+ const update = await confirm(' Update persona?');
93
+ if (update) {
94
+ await onboard(config);
95
+ }
96
+ }
97
+ else {
98
+ console.error('Step 4: Build persona\n');
99
+ await runSkippable('Persona', () => onboard(config));
100
+ }
101
+ // Step 5: Platforms
102
+ console.error('Step 5: Platform setup\n');
103
+ await runSkippable('Platform setup', () => stepPlatforms(config));
104
+ // Step 6: Sync (always runs)
105
+ console.error('Step 6: Sync\n');
106
+ const syncResult = await sync(config, { quiet: false, force: false, dryRun: false });
107
+ // Step 7: Migrate local files
108
+ const unknownFiles = getMemoryFiles(config);
109
+ const personaNotes = await fetchPersonaNotes(config.supabase);
110
+ const knownFiles = new Set(personaNotes.map(n => n.metadata.local_file).filter(Boolean));
111
+ const unknowns = unknownFiles.filter(f => !knownFiles.has(f));
112
+ if (unknowns.length === 0) {
113
+ console.error('Step 7: Migration: no unknown files\n');
114
+ }
115
+ else {
116
+ console.error(`Step 7: Migration: ${unknowns.length} unknown file(s) found\n`);
117
+ console.error(' Run `ledger migrate` to process these files.\n');
118
+ }
119
+ // Summary
120
+ console.error('='.repeat(40));
121
+ console.error('Wizard complete.\n');
122
+ const parts = [
123
+ syncResult.downloaded.length > 0 ? `${syncResult.downloaded.length} downloaded` : null,
124
+ syncResult.uploaded.length > 0 ? `${syncResult.uploaded.length} uploaded` : null,
125
+ syncResult.conflicts.length > 0 ? `${syncResult.conflicts.length} conflicts` : null,
126
+ ].filter(Boolean);
127
+ if (parts.length > 0) {
128
+ console.error(` Sync: ${parts.join(', ')}`);
129
+ }
130
+ console.error(' Run `ledger show <query>` to search your knowledge.');
131
+ }
132
+ function detectAllSteps() {
133
+ const credentials = hasCredentials();
134
+ let database = false;
135
+ let noteCount = 0;
136
+ let device = false;
137
+ let deviceAlias = '';
138
+ let persona = false;
139
+ if (credentials) {
140
+ // We can't check database without connecting, so we'll trust config
141
+ // The actual connection test happens in step 2
142
+ const creds = readCredentials();
143
+ database = creds !== null; // If we can read creds, assume DB was set up before
144
+ }
145
+ const configFile = loadConfigFile();
146
+ if (configFile.device?.alias) {
147
+ device = true;
148
+ deviceAlias = configFile.device.alias;
149
+ }
150
+ // We can't check persona or noteCount without connecting — these will be
151
+ // checked at runtime if credentials exist. For the allDone check,
152
+ // we conservatively say not all done if we can't verify.
153
+ return {
154
+ credentials,
155
+ database,
156
+ noteCount,
157
+ device,
158
+ deviceAlias,
159
+ persona,
160
+ allDone: false, // Full allDone check requires DB connection, done in showAlreadySetUp
161
+ };
162
+ }
163
+ async function showAlreadySetUp(checks) {
164
+ // If we got here, credentials exist. Connect to verify everything.
165
+ const creds = readCredentials();
166
+ if (!creds)
167
+ return;
168
+ let connectResult;
169
+ try {
170
+ const supabase = createClient(creds.supabaseUrl, creds.supabaseKey);
171
+ const { count } = await supabase
172
+ .from('notes')
173
+ .select('*', { count: 'exact', head: true });
174
+ const noteCount = count ?? 0;
175
+ const openai = new OpenAI({ apiKey: creds.openaiKey });
176
+ // Check persona
177
+ const { data: personaData } = await supabase
178
+ .from('notes')
179
+ .select('id')
180
+ .eq('metadata->>delivery', 'persona')
181
+ .limit(1);
182
+ const hasPersona = personaData !== null && personaData.length > 0;
183
+ // Check platforms
184
+ const claudeCode = detectPlatform('claude-code');
185
+ const openclaw = detectPlatform('openclaw');
186
+ const platforms = [
187
+ claudeCode.installed ? 'Claude Code' : null,
188
+ openclaw.installed ? 'OpenClaw' : null,
189
+ ].filter(Boolean);
190
+ const configFile = loadConfigFile();
191
+ // Now check if truly everything is set up
192
+ const allDone = hasPersona && checks.credentials;
193
+ if (!allDone) {
194
+ // Not everything is done — let the main flow handle it
195
+ // Reset and run wizard normally
196
+ checks.noteCount = noteCount;
197
+ checks.persona = hasPersona;
198
+ checks.database = true;
199
+ return;
200
+ }
201
+ console.error('Ledger is already set up.');
202
+ console.error(` Credentials: found (Supabase + OpenAI)`);
203
+ console.error(` Database: connected (${noteCount} notes)`);
204
+ console.error(` Device: ${configFile.device?.alias || '(not set)'}`);
205
+ console.error(` Persona: found`);
206
+ console.error(` Platforms: ${platforms.length > 0 ? platforms.join(', ') : '(none)'}`);
207
+ console.error('');
208
+ const rerun = await ask('Re-run a step? [1-7 or Enter to skip] ');
209
+ if (!rerun)
210
+ return;
211
+ const step = parseInt(rerun, 10);
212
+ if (step < 1 || step > 7)
213
+ return;
214
+ // Build config for re-running
215
+ const config = {
216
+ memoryDir: configFile.memoryDir || resolve(homedir(), `.claude/projects/${homedir().replace(/\//g, '-')}/memory`),
217
+ claudeMdPath: configFile.claudeMdPath || resolve(homedir(), 'CLAUDE.md'),
218
+ supabase,
219
+ openai,
220
+ };
221
+ // Re-running steps 1-2 re-runs all subsequent steps
222
+ if (step <= 2) {
223
+ // Re-run from the beginning by falling through
224
+ console.error('Re-running from step 1 will re-run all subsequent steps.\n');
225
+ const confirmRerun = await confirm('Continue?');
226
+ if (!confirmRerun)
227
+ return;
228
+ // Recursively call wizard (which won't hit allDone since we're forcing re-run)
229
+ // For simplicity, just call the individual steps
230
+ if (step === 1) {
231
+ await gatherCredentials();
232
+ const newCreds = readCredentials();
233
+ await connectAndMigrate(newCreds);
234
+ }
235
+ else {
236
+ await connectAndMigrate(creds);
237
+ }
238
+ await stepDeviceAlias(config);
239
+ await onboard(config);
240
+ await stepPlatforms(config);
241
+ await sync(config, { quiet: false, force: false, dryRun: false });
242
+ return;
243
+ }
244
+ switch (step) {
245
+ case 3:
246
+ await stepDeviceAlias(config);
247
+ break;
248
+ case 4:
249
+ await onboard(config);
250
+ break;
251
+ case 5:
252
+ await stepPlatforms(config);
253
+ break;
254
+ case 6:
255
+ await sync(config, { quiet: false, force: false, dryRun: false });
256
+ break;
257
+ case 7: {
258
+ const { migrate } = await import('./migrate.js');
259
+ await migrate(config);
260
+ break;
261
+ }
262
+ }
263
+ }
264
+ catch {
265
+ // Connection failed — re-run wizard from scratch
266
+ console.error('Could not verify setup. Running wizard...\n');
267
+ }
268
+ }
269
+ // --- Step implementations ---
270
+ async function stepDeviceAlias(config) {
271
+ const configFile = loadConfigFile();
272
+ const current = configFile.device?.alias;
273
+ if (current) {
274
+ console.error(` Current device alias: ${current}`);
275
+ const change = await confirm(' Change it?');
276
+ if (!change)
277
+ return;
278
+ }
279
+ const alias = await ask(' Name this device? (optional, press Enter to skip) ');
280
+ if (!alias)
281
+ return;
282
+ // Save to config.json
283
+ const ledgerDir = getLedgerDir();
284
+ const configPath = resolve(ledgerDir, 'config.json');
285
+ const updated = { ...configFile, device: { alias } };
286
+ writeFileSync(configPath, JSON.stringify(updated, null, 2) + '\n');
287
+ console.error(` Device alias set to "${alias}"\n`);
288
+ // Update user-devices note in Ledger
289
+ const today = new Date().toISOString().split('T')[0];
290
+ const { data: existing } = await config.supabase
291
+ .from('notes')
292
+ .select('id, content, metadata')
293
+ .eq('metadata->>upsert_key', 'user-devices')
294
+ .limit(1)
295
+ .single();
296
+ if (existing) {
297
+ // Check if device already listed
298
+ if (!existing.content.includes(alias)) {
299
+ const newContent = `${existing.content}\n- ${alias} (registered ${today})`;
300
+ await config.supabase
301
+ .from('notes')
302
+ .update({ content: newContent, updated_at: new Date().toISOString() })
303
+ .eq('id', existing.id);
304
+ console.error(` Added "${alias}" to device registry.\n`);
305
+ }
306
+ }
307
+ else {
308
+ // Create device registry note
309
+ const content = `## Devices\n- ${alias} (registered ${today})`;
310
+ const openai = config.openai;
311
+ const embeddingResponse = await openai.embeddings.create({
312
+ model: 'text-embedding-3-small',
313
+ input: content,
314
+ });
315
+ await config.supabase
316
+ .from('notes')
317
+ .insert({
318
+ content,
319
+ metadata: {
320
+ type: 'reference',
321
+ delivery: 'knowledge',
322
+ agent: 'ledger-wizard',
323
+ scope: 'user',
324
+ upsert_key: 'user-devices',
325
+ description: 'Registry of all devices connected to this Ledger instance.',
326
+ },
327
+ embedding: embeddingResponse.data[0].embedding,
328
+ });
329
+ console.error(` Created device registry with "${alias}".\n`);
330
+ }
331
+ }
332
+ async function stepPlatforms(_config) {
333
+ const platforms = ['claude-code', 'openclaw', 'chatgpt'];
334
+ for (const name of platforms) {
335
+ const status = detectPlatform(name);
336
+ const label = name === 'claude-code' ? 'Claude Code' : name === 'openclaw' ? 'OpenClaw' : 'ChatGPT';
337
+ if (status.installed) {
338
+ console.error(` ${label} (installed)`);
339
+ const action = await choose(` Action for ${label}:`, ['Keep', 'Reinstall', 'Uninstall']);
340
+ if (action === 'Reinstall') {
341
+ console.error(` Reinstalling ${label}...\n`);
342
+ if (name === 'claude-code') {
343
+ uninstallClaudeCode();
344
+ await setupClaudeCode();
345
+ }
346
+ else if (name === 'openclaw') {
347
+ uninstallOpenclaw();
348
+ await setupOpenclaw();
349
+ }
350
+ }
351
+ else if (action === 'Uninstall') {
352
+ console.error(` Uninstalling ${label}...\n`);
353
+ if (name === 'claude-code') {
354
+ uninstallClaudeCode();
355
+ }
356
+ else if (name === 'openclaw') {
357
+ uninstallOpenclaw();
358
+ }
359
+ }
360
+ else {
361
+ console.error(` Keeping ${label}.\n`);
362
+ }
363
+ }
364
+ else {
365
+ // ChatGPT never shows as installed but is always available to install
366
+ if (name === 'chatgpt') {
367
+ console.error(` ${label} (static snapshot)`);
368
+ }
369
+ else if (status.detail === 'Claude Code CLI not found') {
370
+ console.error(` ${label}: CLI not found — skipping. Install it and run 'ledger setup claude-code' later.\n`);
371
+ continue;
372
+ }
373
+ else {
374
+ console.error(` ${label} (not installed)`);
375
+ }
376
+ const action = await choose(` Action for ${label}:`, ['Install', 'Skip']);
377
+ if (action === 'Install') {
378
+ console.error(` Installing ${label}...\n`);
379
+ if (name === 'claude-code') {
380
+ await setupClaudeCode();
381
+ }
382
+ else if (name === 'openclaw') {
383
+ await setupOpenclaw();
384
+ }
385
+ else {
386
+ await setupChatgpt();
387
+ }
388
+ }
389
+ else {
390
+ console.error(` Skipped.\n`);
391
+ }
392
+ }
393
+ }
394
+ }
395
+ // --- Error handling wrappers ---
396
+ async function runNonSkippable(label, fn) {
397
+ while (true) {
398
+ try {
399
+ await fn();
400
+ return ok();
401
+ }
402
+ catch (e) {
403
+ console.error(` ${label} failed: ${e.message}\n`);
404
+ const action = await choose(' What to do?', ['Retry', 'Quit']);
405
+ if (action === 'Quit') {
406
+ console.error('Wizard cancelled.');
407
+ return failed(e.message);
408
+ }
409
+ }
410
+ }
411
+ }
412
+ async function runSkippable(label, fn) {
413
+ try {
414
+ await fn();
415
+ return ok();
416
+ }
417
+ catch (e) {
418
+ console.error(` ${label} failed: ${e.message}\n`);
419
+ const action = await choose(' What to do?', ['Retry', 'Skip', 'Quit']);
420
+ if (action === 'Quit') {
421
+ console.error('Wizard cancelled.');
422
+ return failed(e.message);
423
+ }
424
+ if (action === 'Skip') {
425
+ return skipped();
426
+ }
427
+ // Retry
428
+ return runSkippable(label, fn);
429
+ }
430
+ }
@@ -1,10 +1,10 @@
1
1
  #!/bin/bash
2
2
  # Session end: run hash-based sync check + temp file alert
3
3
 
4
- OUTPUT=$(ledger check 2>/dev/null)
4
+ OUTPUT=$(ledger sync --dry-run 2>&1)
5
5
 
6
- # Only show output if there are issues (not "All synced.")
7
- if ! echo "$OUTPUT" | grep -q "All synced"; then
6
+ # Only show output if there are changes to sync
7
+ if ! echo "$OUTPUT" | grep -q "nothing to do"; then
8
8
  echo "$OUTPUT"
9
9
  fi
10
10
 
@@ -0,0 +1,22 @@
1
+ #!/bin/bash
2
+ # Block git commits that contain AI co-author attribution
3
+ # PreToolUse hook on Bash — intercepts before execution
4
+ INPUT=$(cat)
5
+ COMMAND=$(echo "$INPUT" | jq -r '.tool_input.command // empty')
6
+
7
+ if [[ -z "$COMMAND" ]]; then
8
+ exit 0
9
+ fi
10
+
11
+ # Only check git commit commands
12
+ if ! echo "$COMMAND" | grep -qE 'git\s+commit'; then
13
+ exit 0
14
+ fi
15
+
16
+ # Check for Co-Authored-By with AI/Claude/bot patterns
17
+ if echo "$COMMAND" | grep -qiE 'Co-Authored-By:.*\b(Claude|Anthropic|AI|GPT|OpenAI|Copilot|bot)\b'; then
18
+ echo "BLOCKED: Do not include AI Co-Authored-By lines in commits. Remove the Co-Authored-By line and retry." >&2
19
+ exit 2
20
+ fi
21
+
22
+ exit 0