@aperdomoll90/ledger-ai 1.4.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.
- package/dist/cli.js +177 -221
- package/dist/commands/add.js +51 -100
- package/dist/commands/backfill.js +55 -0
- package/dist/commands/backup.js +10 -10
- package/dist/commands/check.js +21 -29
- package/dist/commands/config.js +13 -12
- package/dist/commands/delete.js +22 -17
- package/dist/commands/eval-judge.js +11 -0
- package/dist/commands/eval.js +321 -0
- package/dist/commands/export.js +8 -10
- package/dist/commands/get.js +9 -0
- package/dist/commands/hunt.js +206 -0
- package/dist/commands/ingest.js +15 -14
- package/dist/commands/init.js +18 -20
- package/dist/commands/list.js +21 -7
- package/dist/commands/migrate.js +11 -11
- package/dist/commands/onboard.js +2 -2
- package/dist/commands/pull.js +3 -2
- package/dist/commands/push.js +8 -8
- package/dist/commands/restore.js +38 -38
- package/dist/commands/show.js +13 -16
- package/dist/commands/sync.js +58 -19
- package/dist/commands/tag.js +20 -14
- package/dist/commands/update.js +50 -18
- package/dist/commands/wizard.js +3 -3
- package/dist/lib/ai-search.js +163 -0
- package/dist/lib/audit.js +19 -0
- package/dist/lib/backfill.js +60 -0
- package/dist/lib/config.js +19 -2
- package/dist/lib/document-classification.js +5 -0
- package/dist/lib/document-fetching.js +77 -0
- package/dist/lib/document-operations.js +150 -0
- package/dist/lib/documents/classification.js +5 -0
- package/dist/lib/documents/fetching.js +89 -0
- package/dist/lib/documents/operations.js +304 -0
- package/dist/lib/domains.js +116 -0
- package/dist/lib/embeddings.js +190 -0
- package/dist/lib/errors.js +3 -1
- package/dist/lib/eval/eval-advanced.js +289 -0
- package/dist/lib/eval/eval-judge-session.js +233 -0
- package/dist/lib/eval/eval-store.js +105 -0
- package/dist/lib/eval/eval.js +303 -0
- package/dist/lib/file-writer.js +23 -0
- package/dist/lib/generators.js +44 -45
- package/dist/lib/hunter-db.js +235 -0
- package/dist/lib/hunter-rss.js +30 -0
- package/dist/lib/hunter-scoring.js +55 -0
- package/dist/lib/hunter-types.js +36 -0
- package/dist/lib/lint-configs.js +20 -0
- package/dist/lib/migrate.js +2 -2
- package/dist/lib/notes.js +173 -59
- package/dist/lib/observability.js +296 -0
- package/dist/lib/op-add-note-types.test.js +7 -6
- package/dist/lib/prompt.js +8 -8
- package/dist/lib/rate-limiter.js +103 -0
- package/dist/lib/search/ai-search.js +396 -0
- package/dist/lib/search/chunk-context-enrichment.js +155 -0
- package/dist/lib/search/embeddings.js +293 -0
- package/dist/lib/search/reranker.js +120 -0
- package/dist/lib/search/semantic-cache.js +53 -0
- package/dist/lib/type-registry.test.js +6 -6
- package/dist/mcp-server.js +553 -66
- package/dist/migrations/migrations/005-audit-log.sql +22 -0
- package/dist/migrations/migrations/005_opportunities.sql +48 -0
- package/dist/migrations/migrations/006-audited-operations.sql +235 -0
- package/dist/migrations/migrations/006_hunt_analytics.sql +38 -0
- package/dist/migrations/migrations/007-eval-golden-judgments.sql +119 -0
- package/dist/migrations/migrations/008-drop-expected-doc-ids.sql +9 -0
- package/dist/migrations/migrations/008-judge-helpers.sql +21 -0
- package/dist/migrations/migrations/009-semantic-cache.sql +216 -0
- package/dist/scripts/batch-grade.js +344 -0
- package/dist/scripts/benchmark-ingestion.js +376 -0
- package/dist/scripts/convert-judgments-to-graded.js +88 -0
- package/dist/scripts/diagnose-first-result.js +333 -0
- package/dist/scripts/drop-golden-query.js +53 -0
- package/dist/scripts/eval-search.js +115 -0
- package/dist/scripts/grade-unjudged-top1.js +138 -0
- package/dist/scripts/hunter-analytics.js +38 -0
- package/dist/scripts/hunter-cron.js +63 -0
- package/dist/scripts/hunter-purge.js +25 -0
- package/dist/scripts/migrate-v2.js +140 -0
- package/dist/scripts/reindex.js +74 -0
- package/dist/scripts/sync-local-docs.js +153 -0
- package/package.json +7 -1
package/dist/commands/export.js
CHANGED
|
@@ -1,21 +1,19 @@
|
|
|
1
1
|
import { writeFileSync, mkdirSync } from 'fs';
|
|
2
2
|
import { resolve, dirname } from 'path';
|
|
3
|
-
import {
|
|
3
|
+
import { searchHybrid } from '../lib/search/ai-search.js';
|
|
4
4
|
import { fatal, ExitCode } from '../lib/errors.js';
|
|
5
|
-
export async function
|
|
6
|
-
const results = await
|
|
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
|
|
8
|
+
fatal('No matching documents found.', ExitCode.DOCUMENT_NOT_FOUND);
|
|
9
9
|
}
|
|
10
|
-
const
|
|
11
|
-
const
|
|
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,
|
|
18
|
-
|
|
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
|
+
}
|
package/dist/commands/ingest.js
CHANGED
|
@@ -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,
|
|
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.
|
|
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
|
|
109
|
-
const
|
|
110
|
-
`${
|
|
111
|
-
...['
|
|
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
|
|
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
|
-
|
|
129
|
+
file_path: filename,
|
|
130
130
|
content_hash: hash,
|
|
131
|
-
|
|
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},
|
|
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
|
|
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
|
-
|
|
222
|
+
file_path: filename,
|
|
222
223
|
content_hash: hash,
|
|
223
|
-
|
|
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},
|
|
234
|
+
console.error(`AUTO: ${filename} → Ledger (note ${data.id}, domain: ${noteDomain}).`);
|
|
234
235
|
}
|
package/dist/commands/init.js
CHANGED
|
@@ -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 +
|
|
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('
|
|
122
|
+
.from('documents')
|
|
123
123
|
.select('id')
|
|
124
124
|
.limit(1);
|
|
125
125
|
const isNew = connError !== null;
|
|
126
|
-
if (isNew && !connError.message.includes('
|
|
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 (
|
|
143
|
-
throw new Error(`OpenAI key invalid: ${
|
|
142
|
+
catch (validationError) {
|
|
143
|
+
throw new Error(`OpenAI key invalid: ${validationError.message}`);
|
|
144
144
|
}
|
|
145
145
|
// Run migrations if new database
|
|
146
|
-
let
|
|
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(
|
|
151
|
-
const sql = readMigration(
|
|
152
|
-
return `-- ${
|
|
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('
|
|
161
|
+
.from('documents')
|
|
162
162
|
.select('id')
|
|
163
163
|
.limit(1);
|
|
164
164
|
if (verifyError) {
|
|
165
|
-
throw new Error('
|
|
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('
|
|
171
|
+
.from('documents')
|
|
172
172
|
.select('*', { count: 'exact', head: true });
|
|
173
|
-
|
|
174
|
-
console.error(`Found existing Ledger with ${
|
|
173
|
+
documentCount = count ?? 0;
|
|
174
|
+
console.error(`Found existing Ledger with ${documentCount} documents.\n`);
|
|
175
175
|
}
|
|
176
|
-
return { supabase, openai,
|
|
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 (
|
|
186
|
-
console.error(
|
|
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
|
|
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
|
}
|
package/dist/commands/list.js
CHANGED
|
@@ -1,10 +1,24 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { listDocuments } from '../lib/documents/fetching.js';
|
|
2
2
|
export async function list(config, options) {
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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
|
-
|
|
9
|
-
|
|
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
|
}
|
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,
|
|
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
|
|
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
|
-
|
|
378
|
+
file_path: filename,
|
|
379
379
|
content_hash: hash,
|
|
380
|
-
|
|
380
|
+
domain: noteDomain,
|
|
381
381
|
});
|
|
382
|
-
console.error(` Updated existing note ${existing.id} (type: ${noteType},
|
|
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
|
-
|
|
393
|
+
file_path: filename,
|
|
394
394
|
content_hash: hash,
|
|
395
|
-
|
|
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},
|
|
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
|
|
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
|
-
|
|
423
|
+
file_path: filePath,
|
|
424
424
|
content_hash: hash,
|
|
425
|
-
|
|
425
|
+
domain: inferDomain('feedback'),
|
|
426
426
|
},
|
|
427
427
|
embedding,
|
|
428
428
|
})
|
package/dist/commands/onboard.js
CHANGED
|
@@ -179,8 +179,8 @@ async function createNote(config, input) {
|
|
|
179
179
|
type,
|
|
180
180
|
agent: 'ledger-onboard',
|
|
181
181
|
upsert_key: upsertKey,
|
|
182
|
-
|
|
183
|
-
|
|
182
|
+
file_path: localFile,
|
|
183
|
+
domain: 'persona',
|
|
184
184
|
content_hash: contentHash(content),
|
|
185
185
|
},
|
|
186
186
|
embedding,
|
package/dist/commands/pull.js
CHANGED
|
@@ -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
|
|
19
|
-
if (!
|
|
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);
|
package/dist/commands/push.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readFileSync, existsSync } from 'fs';
|
|
2
2
|
import { resolve, basename } from 'path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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
|
-
|
|
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
|
|
17
|
+
fatal(`No Ledger document matching "${filename}" found. Add it via MCP first.`, ExitCode.DOCUMENT_NOT_FOUND);
|
|
16
18
|
}
|
|
17
|
-
await
|
|
18
|
-
|
|
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
|
}
|