@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.
- 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 +80 -26
- 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/cli.js
CHANGED
|
@@ -4,16 +4,23 @@ import { loadConfig } from './lib/config.js';
|
|
|
4
4
|
import { pull } from './commands/pull.js';
|
|
5
5
|
import { push } from './commands/push.js';
|
|
6
6
|
import { check } from './commands/check.js';
|
|
7
|
+
import { sync } from './commands/sync.js';
|
|
7
8
|
import { show } from './commands/show.js';
|
|
8
9
|
import { exportNote } from './commands/export.js';
|
|
9
10
|
import { ingest } from './commands/ingest.js';
|
|
10
11
|
import { init } from './commands/init.js';
|
|
12
|
+
import { wizard } from './commands/wizard.js';
|
|
11
13
|
import { setupClaudeCode, setupOpenclaw, setupChatgpt } from './commands/setup.js';
|
|
12
14
|
import { backup, enableBackupCron, disableBackupCron } from './commands/backup.js';
|
|
13
15
|
import { restore } from './commands/restore.js';
|
|
14
16
|
import { onboard } from './commands/onboard.js';
|
|
15
17
|
import { configGet, configSet, configList } from './commands/config.js';
|
|
16
18
|
import { migrate } from './commands/migrate.js';
|
|
19
|
+
import { add } from './commands/add.js';
|
|
20
|
+
import { update } from './commands/update.js';
|
|
21
|
+
import { deleteNote } from './commands/delete.js';
|
|
22
|
+
import { list } from './commands/list.js';
|
|
23
|
+
import { tag } from './commands/tag.js';
|
|
17
24
|
process.on('unhandledRejection', (err) => {
|
|
18
25
|
console.error(err instanceof Error ? err.message : String(err));
|
|
19
26
|
process.exit(1);
|
|
@@ -41,11 +48,25 @@ program
|
|
|
41
48
|
});
|
|
42
49
|
program
|
|
43
50
|
.command('check')
|
|
44
|
-
.description('Compare local files vs Ledger, report sync status')
|
|
51
|
+
.description('Compare local files vs Ledger, report sync status (alias for sync --dry-run)')
|
|
45
52
|
.action(async () => {
|
|
46
53
|
const config = loadConfig();
|
|
47
54
|
await check(config);
|
|
48
55
|
});
|
|
56
|
+
program
|
|
57
|
+
.command('sync')
|
|
58
|
+
.description('Bidirectional sync of persona files between Ledger and local cache')
|
|
59
|
+
.option('-q, --quiet', 'suppress output unless conflicts (for hooks)')
|
|
60
|
+
.option('-f, --force', 'overwrite local with Ledger version')
|
|
61
|
+
.option('-n, --dry-run', 'show what would happen without doing it')
|
|
62
|
+
.action(async (options) => {
|
|
63
|
+
const config = loadConfig();
|
|
64
|
+
await sync(config, {
|
|
65
|
+
quiet: options.quiet ?? false,
|
|
66
|
+
force: options.force ?? false,
|
|
67
|
+
dryRun: options.dryRun ?? false,
|
|
68
|
+
});
|
|
69
|
+
});
|
|
49
70
|
program
|
|
50
71
|
.command('show <query...>')
|
|
51
72
|
.description('Search Ledger by meaning, open matching note')
|
|
@@ -126,9 +147,15 @@ program
|
|
|
126
147
|
});
|
|
127
148
|
program
|
|
128
149
|
.command('init')
|
|
129
|
-
.description('
|
|
130
|
-
.
|
|
131
|
-
|
|
150
|
+
.description('Guided setup wizard (credentials, database, persona, platforms, sync)')
|
|
151
|
+
.option('--legacy', 'run legacy init (credentials + database only)')
|
|
152
|
+
.action(async (options) => {
|
|
153
|
+
if (options.legacy) {
|
|
154
|
+
await init();
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
await wizard();
|
|
158
|
+
}
|
|
132
159
|
});
|
|
133
160
|
const setupCmd = program
|
|
134
161
|
.command('setup')
|
|
@@ -158,4 +185,66 @@ program
|
|
|
158
185
|
const config = loadConfig();
|
|
159
186
|
await migrate(config);
|
|
160
187
|
});
|
|
188
|
+
program
|
|
189
|
+
.command('add')
|
|
190
|
+
.description('Add a new note to Ledger (with duplicate detection)')
|
|
191
|
+
.requiredOption('-c, --content <content>', 'note content (or use stdin)')
|
|
192
|
+
.requiredOption('-t, --type <type>', 'note type (feedback, reference, event, etc.)')
|
|
193
|
+
.option('-a, --agent <agent>', 'agent name', 'cli')
|
|
194
|
+
.option('-p, --project <project>', 'project name')
|
|
195
|
+
.option('-k, --upsert-key <key>', 'upsert key for dedup')
|
|
196
|
+
.option('-f, --force', 'skip duplicate check')
|
|
197
|
+
.action(async (options) => {
|
|
198
|
+
const config = loadConfig();
|
|
199
|
+
await add(config, options.content, {
|
|
200
|
+
type: options.type,
|
|
201
|
+
agent: options.agent,
|
|
202
|
+
project: options.project,
|
|
203
|
+
upsertKey: options.upsertKey,
|
|
204
|
+
force: options.force ?? false,
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
program
|
|
208
|
+
.command('update <id>')
|
|
209
|
+
.description('Update an existing note by ID (with confirmation)')
|
|
210
|
+
.requiredOption('-c, --content <content>', 'new content')
|
|
211
|
+
.action(async (id, options) => {
|
|
212
|
+
const config = loadConfig();
|
|
213
|
+
await update(config, parseInt(id, 10), options.content, {});
|
|
214
|
+
});
|
|
215
|
+
program
|
|
216
|
+
.command('delete <id>')
|
|
217
|
+
.description('Delete a note by ID (with confirmation)')
|
|
218
|
+
.action(async (id) => {
|
|
219
|
+
const config = loadConfig();
|
|
220
|
+
await deleteNote(config, parseInt(id, 10));
|
|
221
|
+
});
|
|
222
|
+
program
|
|
223
|
+
.command('list')
|
|
224
|
+
.description('List recent notes from Ledger')
|
|
225
|
+
.option('-n, --limit <number>', 'number of notes', '20')
|
|
226
|
+
.option('-t, --type <type>', 'filter by note type')
|
|
227
|
+
.option('-p, --project <project>', 'filter by project name')
|
|
228
|
+
.action(async (options) => {
|
|
229
|
+
const config = loadConfig();
|
|
230
|
+
await list(config, {
|
|
231
|
+
limit: parseInt(options.limit, 10),
|
|
232
|
+
type: options.type,
|
|
233
|
+
project: options.project,
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
program
|
|
237
|
+
.command('tag <id>')
|
|
238
|
+
.description('Update metadata on a note (description, project, scope)')
|
|
239
|
+
.option('-d, --description <text>', 'note description/purpose')
|
|
240
|
+
.option('-p, --project <name>', 'project name')
|
|
241
|
+
.option('-s, --scope <scope>', 'scope (user, system, general)')
|
|
242
|
+
.action(async (id, options) => {
|
|
243
|
+
const config = loadConfig();
|
|
244
|
+
await tag(config, parseInt(id, 10), {
|
|
245
|
+
description: options.description,
|
|
246
|
+
project: options.project,
|
|
247
|
+
scope: options.scope,
|
|
248
|
+
});
|
|
249
|
+
});
|
|
161
250
|
program.parse();
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import { opAddNote } from '../lib/notes.js';
|
|
2
|
+
import { confirm } from '../lib/prompt.js';
|
|
3
|
+
export async function add(config, content, options) {
|
|
4
|
+
const metadata = {};
|
|
5
|
+
if (options.project)
|
|
6
|
+
metadata.project = options.project;
|
|
7
|
+
if (options.upsertKey)
|
|
8
|
+
metadata.upsert_key = options.upsertKey;
|
|
9
|
+
const result = await opAddNote({ supabase: config.supabase, openai: config.openai }, content, options.type, options.agent || 'cli', metadata, options.force ?? false);
|
|
10
|
+
if (result.status === 'confirm') {
|
|
11
|
+
console.error(result.message);
|
|
12
|
+
const proceed = await confirm('\nCreate new note anyway?');
|
|
13
|
+
if (proceed) {
|
|
14
|
+
const forced = await opAddNote({ supabase: config.supabase, openai: config.openai }, content, options.type, options.agent || 'cli', metadata, true);
|
|
15
|
+
console.error(forced.message);
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
console.error('Cancelled.');
|
|
19
|
+
}
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
console.error(result.message);
|
|
23
|
+
if (result.status === 'error')
|
|
24
|
+
process.exit(1);
|
|
25
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { opDeleteNote } from '../lib/notes.js';
|
|
2
|
+
import { confirm } from '../lib/prompt.js';
|
|
3
|
+
export async function deleteNote(config, id) {
|
|
4
|
+
const clients = { supabase: config.supabase, openai: config.openai };
|
|
5
|
+
// First call: show confirmation
|
|
6
|
+
const preview = await opDeleteNote(clients, id, 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 deletion?');
|
|
13
|
+
if (!proceed) {
|
|
14
|
+
console.error('Cancelled.');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
// Second call: execute
|
|
18
|
+
const result = await opDeleteNote(clients, id, true);
|
|
19
|
+
console.error(result.message);
|
|
20
|
+
if (result.status === 'error')
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
package/dist/commands/ingest.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { readFileSync, unlinkSync, readdirSync, existsSync } from 'fs';
|
|
2
2
|
import { resolve, basename } from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchPersonaNotes, findNoteByFile, searchNotes, inferDelivery } from '../lib/notes.js';
|
|
4
4
|
import { contentHash } from '../lib/hash.js';
|
|
5
5
|
import { confirm, choose } from '../lib/prompt.js';
|
|
6
6
|
export async function ingest(config, options) {
|
|
7
|
-
const existingNotes = await
|
|
7
|
+
const existingNotes = await fetchPersonaNotes(config.supabase);
|
|
8
8
|
if (options.file) {
|
|
9
9
|
if (options.auto) {
|
|
10
10
|
await autoIngestFile(config, resolve(options.file), existingNotes);
|
|
@@ -105,6 +105,12 @@ async function ingestFile(config, filePath, existingNotes) {
|
|
|
105
105
|
'error',
|
|
106
106
|
'general',
|
|
107
107
|
]);
|
|
108
|
+
const defaultDelivery = inferDelivery(noteType);
|
|
109
|
+
const deliveryChoice = await choose(`Delivery tier (default: ${defaultDelivery}):`, [
|
|
110
|
+
`${defaultDelivery} (default)`,
|
|
111
|
+
...['persona', 'project', 'knowledge'].filter(d => d !== defaultDelivery),
|
|
112
|
+
]);
|
|
113
|
+
const delivery = deliveryChoice.replace(' (default)', '');
|
|
108
114
|
const { openai } = config;
|
|
109
115
|
const embeddingResponse = await openai.embeddings.create({
|
|
110
116
|
model: 'text-embedding-3-small',
|
|
@@ -122,7 +128,7 @@ async function ingestFile(config, filePath, existingNotes) {
|
|
|
122
128
|
upsert_key: upsertKey,
|
|
123
129
|
local_file: filename,
|
|
124
130
|
content_hash: hash,
|
|
125
|
-
|
|
131
|
+
delivery,
|
|
126
132
|
},
|
|
127
133
|
embedding,
|
|
128
134
|
})
|
|
@@ -132,7 +138,7 @@ async function ingestFile(config, filePath, existingNotes) {
|
|
|
132
138
|
console.error(`Error adding note: ${error.message}`);
|
|
133
139
|
return;
|
|
134
140
|
}
|
|
135
|
-
console.error(`Added "${filename}" → Ledger (note ${data.id})`);
|
|
141
|
+
console.error(`Added "${filename}" → Ledger (note ${data.id}, delivery: ${delivery})`);
|
|
136
142
|
await askDeleteLocal(filePath, filename);
|
|
137
143
|
}
|
|
138
144
|
async function updateAndHash(config, noteId, content) {
|
|
@@ -176,10 +182,18 @@ async function autoIngestFile(config, filePath, existingNotes) {
|
|
|
176
182
|
// Check for exact duplicate — skip silently if identical
|
|
177
183
|
const exactMatch = existingNotes.find(n => n.metadata.content_hash === hash);
|
|
178
184
|
if (exactMatch) {
|
|
179
|
-
|
|
180
|
-
|
|
185
|
+
console.error(`AUTO: ${filename} — identical to existing note, skipped.`);
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
// Check if a note already exists for this file (by local_file or upsert_key)
|
|
189
|
+
const existingNote = await findNoteByFile(config.supabase, filename);
|
|
190
|
+
if (existingNote) {
|
|
191
|
+
// Update existing note instead of creating a duplicate
|
|
192
|
+
await updateAndHash(config, existingNote.id, content);
|
|
193
|
+
console.error(`AUTO: ${filename} — updated existing note ${existingNote.id}.`);
|
|
181
194
|
return;
|
|
182
195
|
}
|
|
196
|
+
// No existing note — create new
|
|
183
197
|
// Infer type from filename
|
|
184
198
|
let noteType = 'general';
|
|
185
199
|
if (filename.startsWith('feedback_'))
|
|
@@ -206,7 +220,7 @@ async function autoIngestFile(config, filePath, existingNotes) {
|
|
|
206
220
|
upsert_key: upsertKey,
|
|
207
221
|
local_file: filename,
|
|
208
222
|
content_hash: hash,
|
|
209
|
-
|
|
223
|
+
delivery: inferDelivery(noteType),
|
|
210
224
|
},
|
|
211
225
|
embedding,
|
|
212
226
|
})
|
|
@@ -216,6 +230,5 @@ async function autoIngestFile(config, filePath, existingNotes) {
|
|
|
216
230
|
console.error(`AUTO: Error ingesting ${filename}: ${error.message}`);
|
|
217
231
|
return;
|
|
218
232
|
}
|
|
219
|
-
|
|
220
|
-
console.error(`AUTO: ${filename} → Ledger (note ${data.id}), deleted local.`);
|
|
233
|
+
console.error(`AUTO: ${filename} → Ledger (note ${data.id}, delivery: ${inferDelivery(noteType)}).`);
|
|
221
234
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -6,16 +6,17 @@ import { ask, askMasked, confirm } from '../lib/prompt.js';
|
|
|
6
6
|
import { getLedgerDir, loadConfigFile, getDefaultConfig } from '../lib/config.js';
|
|
7
7
|
import { getMigrationFiles, readMigration } from '../lib/migrate.js';
|
|
8
8
|
import { enableBackupCron } from './backup.js';
|
|
9
|
-
|
|
9
|
+
// --- Extracted helpers ---
|
|
10
|
+
/** Gather or load credentials. Returns raw values for use before loadConfig() is safe. */
|
|
11
|
+
export async function gatherCredentials() {
|
|
10
12
|
const ledgerDir = getLedgerDir();
|
|
11
13
|
const envPath = resolve(ledgerDir, '.env');
|
|
12
14
|
const configPath = resolve(ledgerDir, 'config.json');
|
|
13
|
-
console.error('Welcome to Ledger.\n');
|
|
14
15
|
mkdirSync(ledgerDir, { recursive: true });
|
|
15
16
|
let supabaseUrl = '';
|
|
16
17
|
let supabaseKey = '';
|
|
17
18
|
let openaiKey = '';
|
|
18
|
-
//
|
|
19
|
+
// Check existing credentials
|
|
19
20
|
if (existsSync(envPath)) {
|
|
20
21
|
const overwrite = await confirm('Existing credentials found. Overwrite?');
|
|
21
22
|
if (!overwrite) {
|
|
@@ -36,7 +37,7 @@ export async function init() {
|
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
}
|
|
39
|
-
//
|
|
40
|
+
// Prompt for credentials if not loaded
|
|
40
41
|
if (!supabaseUrl) {
|
|
41
42
|
const hasProject = await confirm('Do you have a Supabase project?');
|
|
42
43
|
if (!hasProject) {
|
|
@@ -64,7 +65,7 @@ To create a Supabase project:
|
|
|
64
65
|
writeFileSync(envPath, envContent, { mode: 0o600 });
|
|
65
66
|
console.error('Credentials saved to ~/.ledger/.env\n');
|
|
66
67
|
}
|
|
67
|
-
//
|
|
68
|
+
// Write/merge config.json
|
|
68
69
|
const existing = loadConfigFile();
|
|
69
70
|
const defaults = getDefaultConfig();
|
|
70
71
|
const merged = {
|
|
@@ -74,33 +75,75 @@ To create a Supabase project:
|
|
|
74
75
|
};
|
|
75
76
|
writeFileSync(configPath, JSON.stringify(merged, null, 2) + '\n');
|
|
76
77
|
console.error('Config saved to ~/.ledger/config.json\n');
|
|
77
|
-
|
|
78
|
+
return { supabaseUrl, supabaseKey, openaiKey };
|
|
79
|
+
}
|
|
80
|
+
/** Check if credentials file exists and has all required keys. */
|
|
81
|
+
export function hasCredentials() {
|
|
82
|
+
const envPath = resolve(getLedgerDir(), '.env');
|
|
83
|
+
if (!existsSync(envPath))
|
|
84
|
+
return false;
|
|
85
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
86
|
+
return content.includes('SUPABASE_URL=') &&
|
|
87
|
+
content.includes('SUPABASE_SERVICE_ROLE_KEY=') &&
|
|
88
|
+
content.includes('OPENAI_API_KEY=');
|
|
89
|
+
}
|
|
90
|
+
/** Read raw credentials from the .env file without prompting. */
|
|
91
|
+
export function readCredentials() {
|
|
92
|
+
const envPath = resolve(getLedgerDir(), '.env');
|
|
93
|
+
if (!existsSync(envPath))
|
|
94
|
+
return null;
|
|
95
|
+
let supabaseUrl = '';
|
|
96
|
+
let supabaseKey = '';
|
|
97
|
+
let openaiKey = '';
|
|
98
|
+
const content = readFileSync(envPath, 'utf-8');
|
|
99
|
+
for (const line of content.split('\n')) {
|
|
100
|
+
const eqIndex = line.indexOf('=');
|
|
101
|
+
if (eqIndex === -1)
|
|
102
|
+
continue;
|
|
103
|
+
const key = line.slice(0, eqIndex);
|
|
104
|
+
const value = line.slice(eqIndex + 1);
|
|
105
|
+
if (key === 'SUPABASE_URL')
|
|
106
|
+
supabaseUrl = value;
|
|
107
|
+
if (key === 'SUPABASE_SERVICE_ROLE_KEY')
|
|
108
|
+
supabaseKey = value;
|
|
109
|
+
if (key === 'OPENAI_API_KEY')
|
|
110
|
+
openaiKey = value;
|
|
111
|
+
}
|
|
112
|
+
if (!supabaseUrl || !supabaseKey || !openaiKey)
|
|
113
|
+
return null;
|
|
114
|
+
return { supabaseUrl, supabaseKey, openaiKey };
|
|
115
|
+
}
|
|
116
|
+
/** Connect to Supabase + OpenAI, run migrations if needed. Returns clients + note count. */
|
|
117
|
+
export async function connectAndMigrate(creds) {
|
|
118
|
+
// Verify Supabase connection
|
|
78
119
|
console.error('Connecting to Supabase...');
|
|
79
|
-
const supabase = createClient(supabaseUrl, supabaseKey);
|
|
120
|
+
const supabase = createClient(creds.supabaseUrl, creds.supabaseKey);
|
|
80
121
|
const { error: connError } = await supabase
|
|
81
|
-
.from('
|
|
82
|
-
.select('
|
|
122
|
+
.from('notes')
|
|
123
|
+
.select('id')
|
|
83
124
|
.limit(1);
|
|
84
|
-
const isNew = connError
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
125
|
+
const isNew = connError !== null;
|
|
126
|
+
if (isNew && !connError.message.includes('notes')) {
|
|
127
|
+
throw new Error(`Connection error: ${connError.message}`);
|
|
128
|
+
}
|
|
129
|
+
if (isNew) {
|
|
130
|
+
console.error('Connected (new database).\n');
|
|
89
131
|
}
|
|
90
|
-
|
|
91
|
-
|
|
132
|
+
else {
|
|
133
|
+
console.error('Connected.\n');
|
|
134
|
+
}
|
|
135
|
+
// Validate OpenAI key
|
|
92
136
|
console.error('Validating OpenAI key...');
|
|
137
|
+
const openai = new OpenAI({ apiKey: creds.openaiKey });
|
|
93
138
|
try {
|
|
94
|
-
const openai = new OpenAI({ apiKey: openaiKey });
|
|
95
139
|
await openai.embeddings.create({ model: 'text-embedding-3-small', input: 'test' });
|
|
96
140
|
console.error('OpenAI key valid.\n');
|
|
97
141
|
}
|
|
98
142
|
catch (e) {
|
|
99
|
-
|
|
100
|
-
console.error('Check your OpenAI API key.');
|
|
101
|
-
process.exit(1);
|
|
143
|
+
throw new Error(`OpenAI key invalid: ${e.message}`);
|
|
102
144
|
}
|
|
103
|
-
//
|
|
145
|
+
// Run migrations if new database
|
|
146
|
+
let noteCount = 0;
|
|
104
147
|
if (isNew) {
|
|
105
148
|
console.error('New database detected. Setting up schema...\n');
|
|
106
149
|
const files = getMigrationFiles();
|
|
@@ -114,14 +157,12 @@ To create a Supabase project:
|
|
|
114
157
|
console.error('='.repeat(60));
|
|
115
158
|
console.error('');
|
|
116
159
|
await ask('Press Enter after running the SQL...');
|
|
117
|
-
// Verify
|
|
118
160
|
const { error: verifyError } = await supabase
|
|
119
161
|
.from('notes')
|
|
120
162
|
.select('id')
|
|
121
163
|
.limit(1);
|
|
122
164
|
if (verifyError) {
|
|
123
|
-
|
|
124
|
-
process.exit(1);
|
|
165
|
+
throw new Error('Notes table not found. Make sure you ran all the SQL above.');
|
|
125
166
|
}
|
|
126
167
|
console.error('Schema verified.\n');
|
|
127
168
|
}
|
|
@@ -129,9 +170,22 @@ To create a Supabase project:
|
|
|
129
170
|
const { count } = await supabase
|
|
130
171
|
.from('notes')
|
|
131
172
|
.select('*', { count: 'exact', head: true });
|
|
132
|
-
|
|
173
|
+
noteCount = count ?? 0;
|
|
174
|
+
console.error(`Found existing Ledger with ${noteCount} notes.\n`);
|
|
175
|
+
}
|
|
176
|
+
return { supabase, openai, noteCount };
|
|
177
|
+
}
|
|
178
|
+
// --- Standalone init command (delegates to helpers) ---
|
|
179
|
+
export async function init() {
|
|
180
|
+
console.error('Welcome to Ledger.\n');
|
|
181
|
+
const creds = await gatherCredentials();
|
|
182
|
+
try {
|
|
183
|
+
await connectAndMigrate(creds);
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
console.error(e.message);
|
|
187
|
+
process.exit(1);
|
|
133
188
|
}
|
|
134
|
-
// Step 7: Offer daily backup
|
|
135
189
|
const wantBackup = await confirm('Enable daily local backup? (Saves all notes to ~/.ledger/backups/ at 1am)');
|
|
136
190
|
if (wantBackup) {
|
|
137
191
|
enableBackupCron();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { opListNotes } from '../lib/notes.js';
|
|
2
|
+
export async function list(config, options) {
|
|
3
|
+
const result = await opListNotes({ supabase: config.supabase, openai: config.openai }, options.limit, options.type, options.project);
|
|
4
|
+
if (result.status === 'error') {
|
|
5
|
+
console.error(result.message);
|
|
6
|
+
process.exit(1);
|
|
7
|
+
}
|
|
8
|
+
// List output goes to stdout (machine-readable)
|
|
9
|
+
console.log(result.message);
|
|
10
|
+
}
|
package/dist/commands/migrate.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync, mkdirSync, cpSync, readdirSync } from 'fs';
|
|
2
2
|
import { resolve, basename, join } from 'path';
|
|
3
3
|
import { homedir } from 'os';
|
|
4
|
-
import { searchNotes } from '../lib/notes.js';
|
|
4
|
+
import { searchNotes, inferDelivery } from '../lib/notes.js';
|
|
5
5
|
import { contentHash } from '../lib/hash.js';
|
|
6
6
|
import { confirm, choose } from '../lib/prompt.js';
|
|
7
7
|
export async function migrate(config) {
|
|
@@ -76,7 +76,7 @@ async function backupExisting(config) {
|
|
|
76
76
|
}
|
|
77
77
|
return backupDir;
|
|
78
78
|
}
|
|
79
|
-
function parseReferences(config) {
|
|
79
|
+
export function parseReferences(config) {
|
|
80
80
|
const referenced = new Set();
|
|
81
81
|
// Parse MEMORY.md for linked files: [name](filename.md)
|
|
82
82
|
const memoryMdPath = resolve(config.memoryDir, 'MEMORY.md');
|
|
@@ -103,7 +103,7 @@ function parseReferences(config) {
|
|
|
103
103
|
}
|
|
104
104
|
return referenced;
|
|
105
105
|
}
|
|
106
|
-
function getMemoryFiles(config) {
|
|
106
|
+
export function getMemoryFiles(config) {
|
|
107
107
|
if (!existsSync(config.memoryDir))
|
|
108
108
|
return [];
|
|
109
109
|
return readdirSync(config.memoryDir)
|
|
@@ -369,6 +369,7 @@ async function uploadNewNote(config, filename, content, hash) {
|
|
|
369
369
|
.eq('metadata->>upsert_key', upsertKey)
|
|
370
370
|
.limit(1)
|
|
371
371
|
.single();
|
|
372
|
+
const delivery = inferDelivery(noteType);
|
|
372
373
|
if (existing) {
|
|
373
374
|
await updateNote(config, existing.id, content, {
|
|
374
375
|
type: noteType,
|
|
@@ -376,9 +377,9 @@ async function uploadNewNote(config, filename, content, hash) {
|
|
|
376
377
|
upsert_key: upsertKey,
|
|
377
378
|
local_file: filename,
|
|
378
379
|
content_hash: hash,
|
|
379
|
-
|
|
380
|
+
delivery,
|
|
380
381
|
});
|
|
381
|
-
console.error(` Updated existing note ${existing.id} (type: ${noteType},
|
|
382
|
+
console.error(` Updated existing note ${existing.id} (type: ${noteType}, delivery: ${delivery})`);
|
|
382
383
|
return;
|
|
383
384
|
}
|
|
384
385
|
const { data, error } = await config.supabase
|
|
@@ -391,7 +392,7 @@ async function uploadNewNote(config, filename, content, hash) {
|
|
|
391
392
|
upsert_key: upsertKey,
|
|
392
393
|
local_file: filename,
|
|
393
394
|
content_hash: hash,
|
|
394
|
-
|
|
395
|
+
delivery,
|
|
395
396
|
},
|
|
396
397
|
embedding,
|
|
397
398
|
})
|
|
@@ -401,7 +402,7 @@ async function uploadNewNote(config, filename, content, hash) {
|
|
|
401
402
|
console.error(` Error uploading: ${error.message}`);
|
|
402
403
|
return;
|
|
403
404
|
}
|
|
404
|
-
console.error(` Uploaded (note ${data.id}, type: ${noteType},
|
|
405
|
+
console.error(` Uploaded (note ${data.id}, type: ${noteType}, delivery: ${delivery})`);
|
|
405
406
|
}
|
|
406
407
|
async function uploadFeedbackNote(config, upsertKey, content) {
|
|
407
408
|
const embeddingResponse = await config.openai.embeddings.create({
|
|
@@ -421,7 +422,7 @@ async function uploadFeedbackNote(config, upsertKey, content) {
|
|
|
421
422
|
upsert_key: upsertKey,
|
|
422
423
|
local_file: localFile,
|
|
423
424
|
content_hash: hash,
|
|
424
|
-
|
|
425
|
+
delivery: inferDelivery('feedback'),
|
|
425
426
|
},
|
|
426
427
|
embedding,
|
|
427
428
|
})
|
package/dist/commands/onboard.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getLedgerDir } from '../lib/config.js';
|
|
2
|
-
import {
|
|
2
|
+
import { fetchPersonaNotes } from '../lib/notes.js';
|
|
3
3
|
import { contentHash } from '../lib/hash.js';
|
|
4
4
|
import { ask, confirm, choose } from '../lib/prompt.js';
|
|
5
5
|
import { existsSync } from 'fs';
|
|
@@ -42,7 +42,7 @@ export async function onboard(config) {
|
|
|
42
42
|
process.exit(1);
|
|
43
43
|
}
|
|
44
44
|
// Check if persona already exists
|
|
45
|
-
const existing = await
|
|
45
|
+
const existing = await fetchPersonaNotes(config.supabase);
|
|
46
46
|
const hasProfile = existing.some(n => n.metadata.type === 'user-preference');
|
|
47
47
|
const hasFeedback = existing.some(n => n.metadata.type === 'feedback');
|
|
48
48
|
if (hasProfile || hasFeedback) {
|
|
@@ -177,7 +177,7 @@ async function createNote(config, input) {
|
|
|
177
177
|
agent: 'ledger-onboard',
|
|
178
178
|
upsert_key: upsertKey,
|
|
179
179
|
local_file: localFile,
|
|
180
|
-
|
|
180
|
+
delivery: 'persona',
|
|
181
181
|
content_hash: contentHash(content),
|
|
182
182
|
},
|
|
183
183
|
embedding,
|
package/dist/commands/pull.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { writeFileSync, readFileSync, mkdirSync, existsSync } from 'fs';
|
|
2
2
|
import { resolve } from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { fetchPersonaNotes, updateNoteHash } from '../lib/notes.js';
|
|
4
4
|
import { contentHash } from '../lib/hash.js';
|
|
5
5
|
import { generateClaudeMd, generateMemoryMd } from '../lib/generators.js';
|
|
6
6
|
export async function pull(config, options) {
|
|
7
7
|
const { quiet, force } = options;
|
|
8
|
-
const notes = await
|
|
8
|
+
const notes = await fetchPersonaNotes(config.supabase);
|
|
9
9
|
if (notes.length === 0) {
|
|
10
10
|
if (!quiet)
|
|
11
11
|
console.error('No cached notes found in Ledger.');
|