@aperdomoll90/ledger-ai 1.3.0 → 1.4.2

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.
Files changed (84) hide show
  1. package/dist/cli.js +177 -221
  2. package/dist/commands/add.js +51 -100
  3. package/dist/commands/backfill.js +55 -0
  4. package/dist/commands/backup.js +10 -10
  5. package/dist/commands/check.js +21 -29
  6. package/dist/commands/config.js +13 -12
  7. package/dist/commands/delete.js +22 -17
  8. package/dist/commands/eval-judge.js +11 -0
  9. package/dist/commands/eval.js +321 -0
  10. package/dist/commands/export.js +8 -10
  11. package/dist/commands/get.js +9 -0
  12. package/dist/commands/hunt.js +206 -0
  13. package/dist/commands/ingest.js +15 -14
  14. package/dist/commands/init.js +18 -20
  15. package/dist/commands/list.js +21 -7
  16. package/dist/commands/migrate.js +11 -11
  17. package/dist/commands/onboard.js +2 -2
  18. package/dist/commands/pull.js +3 -2
  19. package/dist/commands/push.js +8 -8
  20. package/dist/commands/restore.js +38 -38
  21. package/dist/commands/show.js +13 -16
  22. package/dist/commands/sync.js +58 -19
  23. package/dist/commands/tag.js +20 -14
  24. package/dist/commands/update.js +50 -18
  25. package/dist/commands/wizard.js +3 -3
  26. package/dist/lib/ai-search.js +163 -0
  27. package/dist/lib/audit.js +19 -0
  28. package/dist/lib/backfill.js +60 -0
  29. package/dist/lib/config.js +19 -2
  30. package/dist/lib/document-classification.js +5 -0
  31. package/dist/lib/document-fetching.js +77 -0
  32. package/dist/lib/document-operations.js +150 -0
  33. package/dist/lib/documents/classification.js +5 -0
  34. package/dist/lib/documents/fetching.js +89 -0
  35. package/dist/lib/documents/operations.js +304 -0
  36. package/dist/lib/domains.js +116 -0
  37. package/dist/lib/embeddings.js +190 -0
  38. package/dist/lib/errors.js +3 -1
  39. package/dist/lib/eval/eval-advanced.js +289 -0
  40. package/dist/lib/eval/eval-judge-session.js +233 -0
  41. package/dist/lib/eval/eval-store.js +105 -0
  42. package/dist/lib/eval/eval.js +303 -0
  43. package/dist/lib/file-writer.js +23 -0
  44. package/dist/lib/generators.js +44 -45
  45. package/dist/lib/hunter-db.js +235 -0
  46. package/dist/lib/hunter-rss.js +30 -0
  47. package/dist/lib/hunter-scoring.js +55 -0
  48. package/dist/lib/hunter-types.js +36 -0
  49. package/dist/lib/lint-configs.js +20 -0
  50. package/dist/lib/migrate.js +2 -2
  51. package/dist/lib/notes.js +173 -59
  52. package/dist/lib/observability.js +296 -0
  53. package/dist/lib/op-add-note-types.test.js +7 -6
  54. package/dist/lib/prompt.js +8 -8
  55. package/dist/lib/rate-limiter.js +103 -0
  56. package/dist/lib/search/ai-search.js +396 -0
  57. package/dist/lib/search/chunk-context-enrichment.js +155 -0
  58. package/dist/lib/search/embeddings.js +293 -0
  59. package/dist/lib/search/reranker.js +120 -0
  60. package/dist/lib/search/semantic-cache.js +53 -0
  61. package/dist/lib/type-registry.test.js +6 -6
  62. package/dist/mcp-server.js +553 -66
  63. package/dist/migrations/migrations/005-audit-log.sql +22 -0
  64. package/dist/migrations/migrations/005_opportunities.sql +48 -0
  65. package/dist/migrations/migrations/006-audited-operations.sql +235 -0
  66. package/dist/migrations/migrations/006_hunt_analytics.sql +38 -0
  67. package/dist/migrations/migrations/007-eval-golden-judgments.sql +119 -0
  68. package/dist/migrations/migrations/008-drop-expected-doc-ids.sql +9 -0
  69. package/dist/migrations/migrations/008-judge-helpers.sql +21 -0
  70. package/dist/migrations/migrations/009-semantic-cache.sql +216 -0
  71. package/dist/scripts/batch-grade.js +344 -0
  72. package/dist/scripts/benchmark-ingestion.js +376 -0
  73. package/dist/scripts/convert-judgments-to-graded.js +88 -0
  74. package/dist/scripts/diagnose-first-result.js +333 -0
  75. package/dist/scripts/drop-golden-query.js +53 -0
  76. package/dist/scripts/eval-search.js +115 -0
  77. package/dist/scripts/grade-unjudged-top1.js +138 -0
  78. package/dist/scripts/hunter-analytics.js +38 -0
  79. package/dist/scripts/hunter-cron.js +63 -0
  80. package/dist/scripts/hunter-purge.js +25 -0
  81. package/dist/scripts/migrate-v2.js +140 -0
  82. package/dist/scripts/reindex.js +74 -0
  83. package/dist/scripts/sync-local-docs.js +153 -0
  84. package/package.json +7 -1
@@ -1,21 +1,19 @@
1
1
  import { writeFileSync, mkdirSync } from 'fs';
2
2
  import { resolve, dirname } from 'path';
3
- import { searchNotes } from '../lib/notes.js';
3
+ import { searchHybrid } from '../lib/search/ai-search.js';
4
4
  import { fatal, ExitCode } from '../lib/errors.js';
5
- export async function exportNote(config, query, outputPath) {
6
- const results = await searchNotes(config.supabase, config.openai, query);
5
+ export async function exportDocument(config, query, outputPath) {
6
+ const results = await searchHybrid({ supabase: config.supabase, openai: config.openai }, { query });
7
7
  if (results.length === 0) {
8
- fatal('No matching notes found.', ExitCode.NOTE_NOT_FOUND);
8
+ fatal('No matching documents found.', ExitCode.DOCUMENT_NOT_FOUND);
9
9
  }
10
- const note = results[0];
11
- const upsertKey = note.metadata.upsert_key || `note-${note.id}`;
12
- const filename = `${upsertKey}.md`;
10
+ const document = results[0];
11
+ const filename = `${document.name}.md`;
13
12
  const targetPath = outputPath
14
13
  ? resolve(outputPath, filename)
15
14
  : resolve(process.cwd(), filename);
16
15
  mkdirSync(dirname(targetPath), { recursive: true });
17
- writeFileSync(targetPath, note.content + '\n', 'utf-8');
18
- // No hash stored — export is untracked
19
- console.log(`Exported "${upsertKey}" → ${targetPath}`);
16
+ writeFileSync(targetPath, document.content + '\n', 'utf-8');
17
+ console.log(`Exported "${document.name}" ${targetPath}`);
20
18
  console.log(targetPath);
21
19
  }
@@ -0,0 +1,9 @@
1
+ import { getDocumentByName } from '../lib/documents/fetching.js';
2
+ import { fatal, ExitCode } from '../lib/errors.js';
3
+ export async function get(config, name) {
4
+ const document = await getDocumentByName(config.supabase, name);
5
+ if (!document) {
6
+ fatal(`Document "${name}" not found.`, ExitCode.DOCUMENT_NOT_FOUND);
7
+ }
8
+ console.log(document.content);
9
+ }
@@ -0,0 +1,206 @@
1
+ import { execFileSync, spawnSync } from 'child_process';
2
+ import { isValidTrack, isValidStatus } from '../lib/hunter-types.js';
3
+ import { getOpportunities, getOpportunityById, getUnscoredOpportunities, updateOpportunityStatus, updateOpportunityNotes, getLatestAnalytics, } from '../lib/hunter-db.js';
4
+ // --- Formatters ---
5
+ function formatOpportunityRow(opp) {
6
+ const score = opp.score !== null ? String(opp.score).padStart(3) : ' -';
7
+ const rec = (opp.recommendation ?? '---').padEnd(5);
8
+ const track = opp.track.padEnd(10);
9
+ const title = opp.title.length > 50 ? opp.title.slice(0, 47) + '...' : opp.title;
10
+ const id = opp.id.slice(0, 8);
11
+ return `${score} ${rec} ${track} ${id} ${title}`;
12
+ }
13
+ function formatOpportunityDetail(opp) {
14
+ const lines = [
15
+ `Title: ${opp.title}`,
16
+ `URL: ${opp.url}`,
17
+ `Track: ${opp.track}`,
18
+ `Score: ${opp.score ?? 'unscored'}`,
19
+ `Recommendation: ${opp.recommendation ?? '-'}`,
20
+ `Status: ${opp.status}`,
21
+ `Source: ${opp.source}`,
22
+ `Found: ${opp.created_at}`,
23
+ ];
24
+ if (opp.summary)
25
+ lines.push(`\nSummary: ${opp.summary}`);
26
+ if (opp.compensation_range)
27
+ lines.push(`Compensation: ${opp.compensation_range}`);
28
+ if (opp.key_requirements?.length)
29
+ lines.push(`Requirements: ${opp.key_requirements.join(', ')}`);
30
+ if (opp.reject_reasons?.length)
31
+ lines.push(`Reject reasons: ${opp.reject_reasons.join(', ')}`);
32
+ if (opp.notes)
33
+ lines.push(`\nNotes: ${opp.notes}`);
34
+ if (opp.score_breakdown) {
35
+ lines.push('\nScore Breakdown:');
36
+ const breakdown = opp.score_breakdown;
37
+ for (const [key, value] of Object.entries(breakdown)) {
38
+ if (key === 'reasoning')
39
+ continue;
40
+ lines.push(` ${key}: ${value}`);
41
+ }
42
+ const reasoning = breakdown.reasoning;
43
+ if (reasoning) {
44
+ lines.push('\nReasoning:');
45
+ for (const [key, value] of Object.entries(reasoning)) {
46
+ lines.push(` ${key}: ${value}`);
47
+ }
48
+ }
49
+ }
50
+ return lines.join('\n');
51
+ }
52
+ // --- Command Handlers ---
53
+ export async function huntStatus(supabase, options) {
54
+ const track = options.track && isValidTrack(options.track) ? options.track : undefined;
55
+ const status = options.status && isValidStatus(options.status) ? options.status : undefined;
56
+ const minScore = options.minScore ? parseInt(options.minScore, 10) : 60;
57
+ const days = options.days ? parseInt(options.days, 10) : 7;
58
+ const limit = options.limit ? parseInt(options.limit, 10) : 25;
59
+ const opportunities = await getOpportunities(supabase, {
60
+ track,
61
+ status,
62
+ minScore,
63
+ days,
64
+ limit,
65
+ });
66
+ if (opportunities.length === 0) {
67
+ console.error('No opportunities found matching filters.');
68
+ return;
69
+ }
70
+ console.error(`\nScore Rec Track ID Title`);
71
+ console.error(`----- ----- ---------- -------- --------------------------------------------------`);
72
+ for (const opp of opportunities) {
73
+ console.error(formatOpportunityRow(opp));
74
+ }
75
+ console.error(`\n${opportunities.length} opportunities shown.`);
76
+ }
77
+ export async function huntShow(supabase, id) {
78
+ const opp = await getOpportunityById(supabase, id);
79
+ if (!opp) {
80
+ console.error(`Opportunity not found: ${id}`);
81
+ return;
82
+ }
83
+ console.error(formatOpportunityDetail(opp));
84
+ }
85
+ export async function huntApplied(supabase, id) {
86
+ await updateOpportunityStatus(supabase, id, 'applied');
87
+ console.error(`Marked ${id.slice(0, 8)} as applied.`);
88
+ }
89
+ export async function huntReject(supabase, id) {
90
+ await updateOpportunityStatus(supabase, id, 'rejected');
91
+ console.error(`Marked ${id.slice(0, 8)} as rejected.`);
92
+ }
93
+ export async function huntNote(supabase, id, text) {
94
+ const opp = await getOpportunityById(supabase, id);
95
+ if (!opp) {
96
+ console.error(`Opportunity not found: ${id}`);
97
+ return;
98
+ }
99
+ const existingNotes = opp.notes ? opp.notes + '\n' : '';
100
+ const timestamp = new Date().toISOString().split('T')[0];
101
+ await updateOpportunityNotes(supabase, id, `${existingNotes}[${timestamp}] ${text}`);
102
+ console.error(`Note added to ${id.slice(0, 8)}.`);
103
+ }
104
+ export async function huntUnscored(supabase) {
105
+ const unscored = await getUnscoredOpportunities(supabase);
106
+ if (unscored.length === 0) {
107
+ console.error('No unscored opportunities.');
108
+ return;
109
+ }
110
+ console.error(`${unscored.length} unscored opportunities waiting.`);
111
+ for (const opp of unscored) {
112
+ console.error(` ${opp.id.slice(0, 8)} ${opp.title.slice(0, 60)}`);
113
+ }
114
+ }
115
+ export async function huntStats(supabase) {
116
+ const analytics = await getLatestAnalytics(supabase);
117
+ if (!analytics) {
118
+ console.error('No analytics data yet. Run `ledger hunt analytics` first.');
119
+ return;
120
+ }
121
+ console.error(`\nAnalytics: ${analytics.period_start} to ${analytics.period_end}`);
122
+ console.error(`---------------------------------------------`);
123
+ console.error(`Total: ${analytics.total_found} | Freelance: ${analytics.freelance_count} | Employment: ${analytics.employment_count}`);
124
+ console.error(`Score 80+: ${analytics.score_80_plus} | 60-79: ${analytics.score_60_79} | 40-59: ${analytics.score_40_59} | <40: ${analytics.score_below_40}`);
125
+ console.error(`Applied: ${analytics.applied_count} | Won: ${analytics.won_count} | Lost: ${analytics.lost_count}`);
126
+ if (analytics.apply_rate !== null)
127
+ console.error(`Apply rate: ${(analytics.apply_rate * 100).toFixed(1)}%`);
128
+ if (analytics.win_rate !== null)
129
+ console.error(`Win rate: ${(analytics.win_rate * 100).toFixed(1)}%`);
130
+ if (analytics.top_reject_reasons) {
131
+ const reasons = Object.entries(analytics.top_reject_reasons)
132
+ .sort((a, b) => b[1] - a[1])
133
+ .slice(0, 5);
134
+ if (reasons.length > 0) {
135
+ console.error(`\nTop reject reasons:`);
136
+ for (const [reason, count] of reasons) {
137
+ console.error(` ${reason}: ${count}`);
138
+ }
139
+ }
140
+ }
141
+ if (analytics.top_skills_demanded) {
142
+ const skills = Object.entries(analytics.top_skills_demanded)
143
+ .sort((a, b) => b[1] - a[1])
144
+ .slice(0, 10);
145
+ if (skills.length > 0) {
146
+ console.error(`\nTop skills demanded:`);
147
+ for (const [skill, count] of skills) {
148
+ console.error(` ${skill}: ${count}`);
149
+ }
150
+ }
151
+ }
152
+ if (analytics.summary) {
153
+ console.error(`\nInsight: ${analytics.summary}`);
154
+ }
155
+ }
156
+ // --- Cron Management ---
157
+ export function enableHunterCron(schedule) {
158
+ const cron = schedule ?? '*/30 8-23 * * *';
159
+ const cronLine = `${cron} ledger-hunter-cron`;
160
+ let existing = '';
161
+ try {
162
+ existing = execFileSync('crontab', ['-l'], { encoding: 'utf-8' });
163
+ }
164
+ catch {
165
+ // No crontab yet
166
+ }
167
+ if (existing.includes('ledger-hunter-cron') || existing.includes('hunter-cron')) {
168
+ console.error('Hunter cron already enabled. Disable first to update.');
169
+ return;
170
+ }
171
+ const newCrontab = existing.trimEnd() + '\n' + cronLine + '\n';
172
+ try {
173
+ const result = spawnSync('crontab', ['-'], { input: newCrontab, stdio: ['pipe', 'pipe', 'pipe'] });
174
+ if (result.status !== 0)
175
+ throw new Error(result.stderr?.toString() || 'crontab failed');
176
+ console.error(`Hunter cron enabled: ${cron}`);
177
+ console.error('View with `crontab -l`.');
178
+ }
179
+ catch (e) {
180
+ console.error(`Failed to set cron: ${e.message}`);
181
+ console.error(`Add manually: ${cronLine}`);
182
+ }
183
+ }
184
+ export function disableHunterCron() {
185
+ let existing = '';
186
+ try {
187
+ existing = execFileSync('crontab', ['-l'], { encoding: 'utf-8' });
188
+ }
189
+ catch {
190
+ console.error('No crontab found.');
191
+ return;
192
+ }
193
+ const filtered = existing
194
+ .split('\n')
195
+ .filter(line => !line.includes('hunter-cron'))
196
+ .join('\n');
197
+ try {
198
+ const result = spawnSync('crontab', ['-'], { input: filtered, stdio: ['pipe', 'pipe', 'pipe'] });
199
+ if (result.status !== 0)
200
+ throw new Error(result.stderr?.toString() || 'crontab failed');
201
+ console.error('Hunter cron disabled.');
202
+ }
203
+ catch (e) {
204
+ console.error(`Failed to update cron: ${e.message}`);
205
+ }
206
+ }
@@ -1,6 +1,6 @@
1
1
  import { readFileSync, unlinkSync, readdirSync, existsSync } from 'fs';
2
2
  import { resolve, basename } from 'path';
3
- import { fetchPersonaNotes, findNoteByFile, searchNotes, inferDelivery } from '../lib/notes.js';
3
+ import { fetchPersonaNotes, findNoteByFile, searchNotes, inferDomain } 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) {
@@ -20,7 +20,7 @@ export async function ingest(config, options) {
20
20
  return;
21
21
  }
22
22
  const knownFiles = new Set(existingNotes
23
- .map(n => n.metadata.local_file)
23
+ .map(n => n.metadata.file_path)
24
24
  .filter(Boolean));
25
25
  const localFiles = readdirSync(config.memoryDir)
26
26
  .filter(f => f.endsWith('.md') && f !== 'MEMORY.md' && !knownFiles.has(f));
@@ -105,12 +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', 'protected'].filter(d => d !== defaultDelivery),
108
+ const defaultDomain = inferDomain(noteType);
109
+ const domainChoice = await choose(`Domain (default: ${defaultDomain}):`, [
110
+ `${defaultDomain} (default)`,
111
+ ...['system', 'persona', 'workspace', 'project'].filter(d => d !== defaultDomain),
112
112
  ]);
113
- const delivery = deliveryChoice.replace(' (default)', '');
113
+ const domain = domainChoice.replace(' (default)', '');
114
114
  const { openai } = config;
115
115
  const embeddingResponse = await openai.embeddings.create({
116
116
  model: 'text-embedding-3-small',
@@ -126,9 +126,9 @@ async function ingestFile(config, filePath, existingNotes) {
126
126
  type: noteType,
127
127
  agent: 'ledger-ingest',
128
128
  upsert_key: upsertKey,
129
- local_file: filename,
129
+ file_path: filename,
130
130
  content_hash: hash,
131
- delivery,
131
+ domain,
132
132
  },
133
133
  embedding,
134
134
  })
@@ -138,7 +138,7 @@ async function ingestFile(config, filePath, existingNotes) {
138
138
  console.error(`Error adding note: ${error.message}`);
139
139
  return;
140
140
  }
141
- console.error(`Added "${filename}" → Ledger (note ${data.id}, delivery: ${delivery})`);
141
+ console.error(`Added "${filename}" → Ledger (note ${data.id}, domain: ${domain})`);
142
142
  await askDeleteLocal(filePath, filename);
143
143
  }
144
144
  async function updateAndHash(config, noteId, content) {
@@ -185,7 +185,7 @@ async function autoIngestFile(config, filePath, existingNotes) {
185
185
  console.error(`AUTO: ${filename} — identical to existing note, skipped.`);
186
186
  return;
187
187
  }
188
- // Check if a note already exists for this file (by local_file or upsert_key)
188
+ // Check if a note already exists for this file (by file_path or upsert_key)
189
189
  const existingNote = await findNoteByFile(config.supabase, filename);
190
190
  if (existingNote) {
191
191
  // Update existing note instead of creating a duplicate
@@ -210,6 +210,7 @@ async function autoIngestFile(config, filePath, existingNotes) {
210
210
  });
211
211
  const embedding = embeddingResponse.data[0].embedding;
212
212
  const upsertKey = filename.replace(/\.md$/, '').replace(/_/g, '-');
213
+ const noteDomain = inferDomain(noteType);
213
214
  const { data, error } = await config.supabase
214
215
  .from('notes')
215
216
  .insert({
@@ -218,9 +219,9 @@ async function autoIngestFile(config, filePath, existingNotes) {
218
219
  type: noteType,
219
220
  agent: 'ledger-auto-ingest',
220
221
  upsert_key: upsertKey,
221
- local_file: filename,
222
+ file_path: filename,
222
223
  content_hash: hash,
223
- delivery: inferDelivery(noteType),
224
+ domain: noteDomain,
224
225
  },
225
226
  embedding,
226
227
  })
@@ -230,5 +231,5 @@ async function autoIngestFile(config, filePath, existingNotes) {
230
231
  console.error(`AUTO: Error ingesting ${filename}: ${error.message}`);
231
232
  return;
232
233
  }
233
- console.error(`AUTO: ${filename} → Ledger (note ${data.id}, delivery: ${inferDelivery(noteType)}).`);
234
+ console.error(`AUTO: ${filename} → Ledger (note ${data.id}, domain: ${noteDomain}).`);
234
235
  }
@@ -113,17 +113,17 @@ export function readCredentials() {
113
113
  return null;
114
114
  return { supabaseUrl, supabaseKey, openaiKey };
115
115
  }
116
- /** Connect to Supabase + OpenAI, run migrations if needed. Returns clients + note count. */
116
+ /** Connect to Supabase + OpenAI, run migrations if needed. Returns clients + document count. */
117
117
  export async function connectAndMigrate(creds) {
118
118
  // Verify Supabase connection
119
119
  console.error('Connecting to Supabase...');
120
120
  const supabase = createClient(creds.supabaseUrl, creds.supabaseKey);
121
121
  const { error: connError } = await supabase
122
- .from('notes')
122
+ .from('documents')
123
123
  .select('id')
124
124
  .limit(1);
125
125
  const isNew = connError !== null;
126
- if (isNew && !connError.message.includes('notes')) {
126
+ if (isNew && !connError.message.includes('documents')) {
127
127
  throw new Error(`Connection error: ${connError.message}`);
128
128
  }
129
129
  if (isNew) {
@@ -139,17 +139,17 @@ export async function connectAndMigrate(creds) {
139
139
  await openai.embeddings.create({ model: 'text-embedding-3-small', input: 'test' });
140
140
  console.error('OpenAI key valid.\n');
141
141
  }
142
- catch (e) {
143
- throw new Error(`OpenAI key invalid: ${e.message}`);
142
+ catch (validationError) {
143
+ throw new Error(`OpenAI key invalid: ${validationError.message}`);
144
144
  }
145
145
  // Run migrations if new database
146
- let noteCount = 0;
146
+ let documentCount = 0;
147
147
  if (isNew) {
148
148
  console.error('New database detected. Setting up schema...\n');
149
149
  const files = getMigrationFiles();
150
- const allSql = files.map(f => {
151
- const sql = readMigration(f);
152
- return `-- ${f}\n${sql}`;
150
+ const allSql = files.map(file => {
151
+ const sql = readMigration(file);
152
+ return `-- ${file}\n${sql}`;
153
153
  }).join('\n\n');
154
154
  console.error('Run the following SQL in Supabase Dashboard > SQL Editor:\n');
155
155
  console.error('='.repeat(60));
@@ -158,22 +158,22 @@ export async function connectAndMigrate(creds) {
158
158
  console.error('');
159
159
  await ask('Press Enter after running the SQL...');
160
160
  const { error: verifyError } = await supabase
161
- .from('notes')
161
+ .from('documents')
162
162
  .select('id')
163
163
  .limit(1);
164
164
  if (verifyError) {
165
- throw new Error('Notes table not found. Make sure you ran all the SQL above.');
165
+ throw new Error('Documents table not found. Make sure you ran all the SQL above.');
166
166
  }
167
167
  console.error('Schema verified.\n');
168
168
  }
169
169
  else {
170
170
  const { count } = await supabase
171
- .from('notes')
171
+ .from('documents')
172
172
  .select('*', { count: 'exact', head: true });
173
- noteCount = count ?? 0;
174
- console.error(`Found existing Ledger with ${noteCount} notes.\n`);
173
+ documentCount = count ?? 0;
174
+ console.error(`Found existing Ledger with ${documentCount} documents.\n`);
175
175
  }
176
- return { supabase, openai, noteCount };
176
+ return { supabase, openai, documentCount };
177
177
  }
178
178
  // --- Standalone init command (delegates to helpers) ---
179
179
  export async function init() {
@@ -182,15 +182,13 @@ export async function init() {
182
182
  try {
183
183
  await connectAndMigrate(creds);
184
184
  }
185
- catch (e) {
186
- console.error(e.message);
185
+ catch (migrationError) {
186
+ console.error(migrationError.message);
187
187
  process.exit(1);
188
188
  }
189
- const wantBackup = await confirm('Enable daily local backup? (Saves all notes to ~/.ledger/backups/ at 1am)');
189
+ const wantBackup = await confirm('Enable daily local backup? (Saves all documents to ~/.ledger/backups/ at 1am)');
190
190
  if (wantBackup) {
191
191
  enableBackupCron();
192
192
  }
193
193
  console.error('\nInit complete.');
194
- console.error('Run `ledger setup <platform>` to connect an agent.');
195
- console.error('Platforms: claude-code, openclaw, chatgpt');
196
194
  }
@@ -1,10 +1,24 @@
1
- import { opListNotes } from '../lib/notes.js';
1
+ import { listDocuments } from '../lib/documents/fetching.js';
2
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);
3
+ const documents = await listDocuments(config.supabase, {
4
+ limit: options.limit,
5
+ document_type: options.type,
6
+ project: options.project,
7
+ domain: options.domain,
8
+ });
9
+ if (documents.length === 0) {
10
+ console.error('No documents found.');
11
+ process.exit(0);
7
12
  }
8
- // List output goes to stdout (machine-readable)
9
- console.log(result.message);
13
+ const formatted = documents.map(document => {
14
+ return [
15
+ `[${document.id}] ${document.name}`,
16
+ ` Domain: ${document.domain} | Type: ${document.document_type}${document.project ? ` | Project: ${document.project}` : ''}`,
17
+ ` Protection: ${document.protection} | Auto-load: ${document.is_auto_load}`,
18
+ document.description ? ` Description: ${document.description}` : null,
19
+ ` Content: ${document.content.slice(0, 150)}${document.content.length > 150 ? '...' : ''}`,
20
+ ` Updated: ${document.updated_at}`,
21
+ ].filter(Boolean).join('\n');
22
+ });
23
+ console.log(formatted.join('\n\n'));
10
24
  }
@@ -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, inferDelivery } from '../lib/notes.js';
4
+ import { searchNotes, inferDomain } 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) {
@@ -369,17 +369,17 @@ 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
+ const noteDomain = inferDomain(noteType);
373
373
  if (existing) {
374
374
  await updateNote(config, existing.id, content, {
375
375
  type: noteType,
376
376
  agent: 'ledger-migrate',
377
377
  upsert_key: upsertKey,
378
- local_file: filename,
378
+ file_path: filename,
379
379
  content_hash: hash,
380
- delivery,
380
+ domain: noteDomain,
381
381
  });
382
- console.error(` Updated existing note ${existing.id} (type: ${noteType}, delivery: ${delivery})`);
382
+ console.error(` Updated existing note ${existing.id} (type: ${noteType}, domain: ${noteDomain})`);
383
383
  return;
384
384
  }
385
385
  const { data, error } = await config.supabase
@@ -390,9 +390,9 @@ async function uploadNewNote(config, filename, content, hash) {
390
390
  type: noteType,
391
391
  agent: 'ledger-migrate',
392
392
  upsert_key: upsertKey,
393
- local_file: filename,
393
+ file_path: filename,
394
394
  content_hash: hash,
395
- delivery,
395
+ domain: noteDomain,
396
396
  },
397
397
  embedding,
398
398
  })
@@ -402,7 +402,7 @@ async function uploadNewNote(config, filename, content, hash) {
402
402
  console.error(` Error uploading: ${error.message}`);
403
403
  return;
404
404
  }
405
- console.error(` Uploaded (note ${data.id}, type: ${noteType}, delivery: ${delivery})`);
405
+ console.error(` Uploaded (note ${data.id}, type: ${noteType}, domain: ${noteDomain})`);
406
406
  }
407
407
  async function uploadFeedbackNote(config, upsertKey, content) {
408
408
  const embeddingResponse = await config.openai.embeddings.create({
@@ -410,7 +410,7 @@ async function uploadFeedbackNote(config, upsertKey, content) {
410
410
  input: content,
411
411
  });
412
412
  const embedding = embeddingResponse.data[0].embedding;
413
- const localFile = upsertKey.replace(/-/g, '_') + '.md';
413
+ const filePath = upsertKey.replace(/-/g, '_') + '.md';
414
414
  const hash = contentHash(content);
415
415
  const { data, error } = await config.supabase
416
416
  .from('notes')
@@ -420,9 +420,9 @@ async function uploadFeedbackNote(config, upsertKey, content) {
420
420
  type: 'feedback',
421
421
  agent: 'ledger-migrate',
422
422
  upsert_key: upsertKey,
423
- local_file: localFile,
423
+ file_path: filePath,
424
424
  content_hash: hash,
425
- delivery: inferDelivery('feedback'),
425
+ domain: inferDomain('feedback'),
426
426
  },
427
427
  embedding,
428
428
  })
@@ -179,8 +179,8 @@ async function createNote(config, input) {
179
179
  type,
180
180
  agent: 'ledger-onboard',
181
181
  upsert_key: upsertKey,
182
- local_file: localFile,
183
- delivery: 'persona',
182
+ file_path: localFile,
183
+ domain: 'persona',
184
184
  content_hash: contentHash(content),
185
185
  },
186
186
  embedding,
@@ -15,9 +15,10 @@ export async function pull(config, options) {
15
15
  const writtenFiles = [];
16
16
  const conflicts = [];
17
17
  for (const note of notes) {
18
- const localFile = note.metadata.local_file;
19
- if (!localFile)
18
+ const noteFilePath = note.metadata.file_path;
19
+ if (!noteFilePath)
20
20
  continue;
21
+ const localFile = noteFilePath.includes('/') ? noteFilePath.split('/').pop() : noteFilePath;
21
22
  const filePath = resolve(config.memoryDir, localFile);
22
23
  const ledgerContent = note.content;
23
24
  const ledgerHash = contentHash(ledgerContent);
@@ -1,7 +1,7 @@
1
1
  import { readFileSync, existsSync } from 'fs';
2
2
  import { resolve, basename } from 'path';
3
- import { findNoteByFile, updateNoteContent, updateNoteHash } from '../lib/notes.js';
4
- import { contentHash } from '../lib/hash.js';
3
+ import { listDocuments } from '../lib/documents/fetching.js';
4
+ import { updateDocument } from '../lib/documents/operations.js';
5
5
  import { fatal, ExitCode } from '../lib/errors.js';
6
6
  export async function push(config, filePath) {
7
7
  const absPath = resolve(filePath);
@@ -10,12 +10,12 @@ export async function push(config, filePath) {
10
10
  }
11
11
  const filename = basename(absPath);
12
12
  const content = readFileSync(absPath, 'utf-8').trim();
13
- const existing = await findNoteByFile(config.supabase, filename);
13
+ // Find document by file_path matching the filename
14
+ const documents = await listDocuments(config.supabase, { limit: 100 });
15
+ const existing = documents.find(document => document.file_path && basename(document.file_path) === filename);
14
16
  if (!existing) {
15
- fatal(`No Ledger note matching "${filename}" found. Add it via MCP first.`, ExitCode.NOTE_NOT_FOUND);
17
+ fatal(`No Ledger document matching "${filename}" found. Add it via MCP first.`, ExitCode.DOCUMENT_NOT_FOUND);
16
18
  }
17
- await updateNoteContent(config.supabase, config.openai, existing.id, content);
18
- const hash = contentHash(content);
19
- await updateNoteHash(config.supabase, existing.id, hash);
20
- console.log(`Pushed ${filename} → Ledger (note ${existing.id})`);
19
+ await updateDocument({ supabase: config.supabase, openai: config.openai }, { id: existing.id, content, agent: 'cli' });
20
+ console.log(`Pushed ${filename} Ledger (document ${existing.id}, "${existing.name}")`);
21
21
  }