@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.
- 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/lib/generators.js
CHANGED
|
@@ -1,4 +1,46 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Generate MEMORY.md as a search guide (v2).
|
|
3
|
+
* NOT a file index — tells agents what knowledge domains exist and when to search.
|
|
4
|
+
*/
|
|
5
|
+
export function generateMemoryMd(autoLoadFiles) {
|
|
6
|
+
const lines = [
|
|
7
|
+
'# What I Know About You',
|
|
8
|
+
'',
|
|
9
|
+
'## Auto-loaded (always in context)',
|
|
10
|
+
'',
|
|
11
|
+
];
|
|
12
|
+
if (autoLoadFiles.length > 0) {
|
|
13
|
+
for (const f of autoLoadFiles) {
|
|
14
|
+
lines.push(`- [${f}](${f})`);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
else {
|
|
18
|
+
lines.push('- Personality, behavioral rules, preferences — loaded from Ledger');
|
|
19
|
+
}
|
|
20
|
+
lines.push('');
|
|
21
|
+
lines.push('## Search Ledger when needed');
|
|
22
|
+
lines.push('');
|
|
23
|
+
lines.push('- **System:** hooks, plugin configs, sync rules — `search_notes` with domain: system');
|
|
24
|
+
lines.push('- **Workspace:** dashboards, device registry, dev environment — `search_notes` with domain: workspace');
|
|
25
|
+
lines.push('- **Projects:** architecture, status, errors, events — `search_notes` with project name or domain: project');
|
|
26
|
+
lines.push('- **Skills:** eval results, test cases — `search_notes` with skill name or type: skill');
|
|
27
|
+
lines.push('');
|
|
28
|
+
return lines.join('\n');
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Generate CLAUDE.md. In v2, prefers the claude-md note stored as a complete document.
|
|
32
|
+
* Falls back to legacy generation from feedback notes if no claude-md note exists.
|
|
33
|
+
* @deprecated v2 stores CLAUDE.md as a complete document (type: claude-md). This fallback will be removed.
|
|
34
|
+
*/
|
|
35
|
+
export function generateClaudeMd(notes) {
|
|
36
|
+
const claudeMdNote = notes.find(n => n.metadata.type === 'claude-md' ||
|
|
37
|
+
n.metadata.upsert_key === 'claude-md-backup');
|
|
38
|
+
if (claudeMdNote) {
|
|
39
|
+
return claudeMdNote.content;
|
|
40
|
+
}
|
|
41
|
+
return legacyGenerateClaudeMd(notes);
|
|
42
|
+
}
|
|
43
|
+
// --- Legacy helpers (pre-v2) ---
|
|
2
44
|
const SECTION_MAP = {
|
|
3
45
|
'Security': ['feedback-no-read-env'],
|
|
4
46
|
'Coding Conventions': ['feedback-coding-conventions'],
|
|
@@ -10,7 +52,6 @@ const SECTION_MAP = {
|
|
|
10
52
|
],
|
|
11
53
|
'Communication': ['feedback-communication-style'],
|
|
12
54
|
};
|
|
13
|
-
// --- Helpers ---
|
|
14
55
|
function extractBulletPoints(content) {
|
|
15
56
|
const lines = content.split('\n');
|
|
16
57
|
const bullets = [];
|
|
@@ -35,8 +76,7 @@ function extractBulletPoints(content) {
|
|
|
35
76
|
}
|
|
36
77
|
return bullets.join('\n');
|
|
37
78
|
}
|
|
38
|
-
|
|
39
|
-
export function generateClaudeMd(notes) {
|
|
79
|
+
function legacyGenerateClaudeMd(notes) {
|
|
40
80
|
const notesByKey = new Map();
|
|
41
81
|
for (const note of notes) {
|
|
42
82
|
const key = note.metadata.upsert_key;
|
|
@@ -72,44 +112,3 @@ export function generateClaudeMd(notes) {
|
|
|
72
112
|
}
|
|
73
113
|
return sections.join('\n') + '\n';
|
|
74
114
|
}
|
|
75
|
-
export function generateMemoryMd(files) {
|
|
76
|
-
const userFiles = [];
|
|
77
|
-
const feedbackFiles = [];
|
|
78
|
-
const projectFiles = [];
|
|
79
|
-
for (const file of files) {
|
|
80
|
-
if (file.startsWith('user_'))
|
|
81
|
-
userFiles.push(file);
|
|
82
|
-
else if (file.startsWith('feedback_'))
|
|
83
|
-
feedbackFiles.push(file);
|
|
84
|
-
else if (file.startsWith('project_'))
|
|
85
|
-
projectFiles.push(file);
|
|
86
|
-
}
|
|
87
|
-
const lines = [
|
|
88
|
-
'# Memory Index',
|
|
89
|
-
'',
|
|
90
|
-
'Local cache files auto-loaded into Claude Code context. Source of truth is Ledger.',
|
|
91
|
-
'',
|
|
92
|
-
];
|
|
93
|
-
if (userFiles.length > 0) {
|
|
94
|
-
lines.push('## User Profile');
|
|
95
|
-
for (const f of userFiles)
|
|
96
|
-
lines.push(`- [${f}](${f})`);
|
|
97
|
-
lines.push('');
|
|
98
|
-
}
|
|
99
|
-
if (feedbackFiles.length > 0) {
|
|
100
|
-
lines.push('## Feedback (Behavioral Rules)');
|
|
101
|
-
for (const f of feedbackFiles)
|
|
102
|
-
lines.push(`- [${f}](${f})`);
|
|
103
|
-
lines.push('');
|
|
104
|
-
}
|
|
105
|
-
if (projectFiles.length > 0) {
|
|
106
|
-
lines.push('## Project Status');
|
|
107
|
-
for (const f of projectFiles)
|
|
108
|
-
lines.push(`- [${f}](${f})`);
|
|
109
|
-
lines.push('');
|
|
110
|
-
}
|
|
111
|
-
lines.push('## Not Auto-Loaded (Search Ledger)');
|
|
112
|
-
lines.push('Architecture, references, project details, events, errors — all in Ledger, search on demand.');
|
|
113
|
-
lines.push('');
|
|
114
|
-
return lines.join('\n');
|
|
115
|
-
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
import { isValidStatus } from './hunter-types.js';
|
|
2
|
+
export function buildInsertRow(listing, track) {
|
|
3
|
+
return {
|
|
4
|
+
url: listing.url,
|
|
5
|
+
title: listing.title,
|
|
6
|
+
raw_description: listing.description,
|
|
7
|
+
source: listing.source,
|
|
8
|
+
track,
|
|
9
|
+
score: null,
|
|
10
|
+
status: 'new',
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function buildStatusUpdate(status) {
|
|
14
|
+
if (!isValidStatus(status)) {
|
|
15
|
+
throw new Error(`Invalid status: ${status}`);
|
|
16
|
+
}
|
|
17
|
+
return {
|
|
18
|
+
status,
|
|
19
|
+
status_changed_at: new Date().toISOString(),
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
export function getScoreThreshold(score) {
|
|
23
|
+
if (score >= 80)
|
|
24
|
+
return 'apply';
|
|
25
|
+
if (score >= 60)
|
|
26
|
+
return 'apply';
|
|
27
|
+
if (score >= 40)
|
|
28
|
+
return 'ask';
|
|
29
|
+
return 'skip';
|
|
30
|
+
}
|
|
31
|
+
export function buildAnalyticsRow(opportunities, periodStart, periodEnd) {
|
|
32
|
+
const freelance = opportunities.filter(o => o.track === 'freelance');
|
|
33
|
+
const employment = opportunities.filter(o => o.track === 'employment');
|
|
34
|
+
const scored = opportunities.filter(o => o.score !== null);
|
|
35
|
+
const applied = opportunities.filter(o => o.status === 'applied');
|
|
36
|
+
const won = opportunities.filter(o => o.status === 'won');
|
|
37
|
+
const lost = opportunities.filter(o => o.status === 'lost');
|
|
38
|
+
const rejectReasons = {};
|
|
39
|
+
for (const opp of opportunities) {
|
|
40
|
+
if (opp.reject_reasons) {
|
|
41
|
+
for (const reason of opp.reject_reasons) {
|
|
42
|
+
rejectReasons[reason] = (rejectReasons[reason] || 0) + 1;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const skills = {};
|
|
47
|
+
for (const opp of opportunities) {
|
|
48
|
+
if (opp.key_requirements) {
|
|
49
|
+
for (const skill of opp.key_requirements) {
|
|
50
|
+
skills[skill] = (skills[skill] || 0) + 1;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const avgByTrack = {};
|
|
55
|
+
for (const track of ['freelance', 'employment']) {
|
|
56
|
+
const trackScored = scored.filter(o => o.track === track);
|
|
57
|
+
if (trackScored.length > 0) {
|
|
58
|
+
const sum = trackScored.reduce((acc, o) => acc + (o.score ?? 0), 0);
|
|
59
|
+
avgByTrack[track] = Math.round(sum / trackScored.length);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return {
|
|
63
|
+
period_start: periodStart,
|
|
64
|
+
period_end: periodEnd,
|
|
65
|
+
total_found: opportunities.length,
|
|
66
|
+
freelance_count: freelance.length,
|
|
67
|
+
employment_count: employment.length,
|
|
68
|
+
score_80_plus: scored.filter(o => (o.score ?? 0) >= 80).length,
|
|
69
|
+
score_60_79: scored.filter(o => (o.score ?? 0) >= 60 && (o.score ?? 0) < 80).length,
|
|
70
|
+
score_40_59: scored.filter(o => (o.score ?? 0) >= 40 && (o.score ?? 0) < 60).length,
|
|
71
|
+
score_below_40: scored.filter(o => (o.score ?? 0) < 40).length,
|
|
72
|
+
applied_count: applied.length,
|
|
73
|
+
won_count: won.length,
|
|
74
|
+
lost_count: lost.length,
|
|
75
|
+
apply_rate: opportunities.length > 0 ? Math.round((applied.length / opportunities.length) * 100) / 100 : null,
|
|
76
|
+
win_rate: applied.length > 0 ? Math.round((won.length / applied.length) * 100) / 100 : null,
|
|
77
|
+
top_reject_reasons: rejectReasons,
|
|
78
|
+
top_skills_demanded: skills,
|
|
79
|
+
avg_score_by_track: avgByTrack,
|
|
80
|
+
compensation_ranges: null,
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
// --- Supabase Operations ---
|
|
84
|
+
export async function insertRawListings(supabase, listings, track) {
|
|
85
|
+
let inserted = 0;
|
|
86
|
+
let skipped = 0;
|
|
87
|
+
for (const listing of listings) {
|
|
88
|
+
const row = buildInsertRow(listing, track);
|
|
89
|
+
const { error } = await supabase
|
|
90
|
+
.from('opportunities')
|
|
91
|
+
.upsert(row, { onConflict: 'url', ignoreDuplicates: true });
|
|
92
|
+
if (error) {
|
|
93
|
+
skipped++;
|
|
94
|
+
}
|
|
95
|
+
else {
|
|
96
|
+
inserted++;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return { inserted, skipped };
|
|
100
|
+
}
|
|
101
|
+
export async function getUnscoredOpportunities(supabase) {
|
|
102
|
+
const { data, error } = await supabase
|
|
103
|
+
.from('opportunities')
|
|
104
|
+
.select('*')
|
|
105
|
+
.is('score', null)
|
|
106
|
+
.order('created_at', { ascending: true });
|
|
107
|
+
if (error)
|
|
108
|
+
throw new Error(`Failed to fetch unscored: ${error.message}`);
|
|
109
|
+
return (data ?? []);
|
|
110
|
+
}
|
|
111
|
+
export async function updateOpportunityScore(supabase, id, score, scoreBreakdown, recommendation, summary, keyRequirements, compensationRange, rejectReasons) {
|
|
112
|
+
const { error } = await supabase
|
|
113
|
+
.from('opportunities')
|
|
114
|
+
.update({
|
|
115
|
+
score,
|
|
116
|
+
score_breakdown: scoreBreakdown,
|
|
117
|
+
recommendation,
|
|
118
|
+
summary,
|
|
119
|
+
key_requirements: keyRequirements,
|
|
120
|
+
compensation_range: compensationRange,
|
|
121
|
+
reject_reasons: rejectReasons,
|
|
122
|
+
scored_at: new Date().toISOString(),
|
|
123
|
+
})
|
|
124
|
+
.eq('id', id);
|
|
125
|
+
if (error)
|
|
126
|
+
throw new Error(`Failed to update score: ${error.message}`);
|
|
127
|
+
}
|
|
128
|
+
export async function updateOpportunityStatus(supabase, id, status) {
|
|
129
|
+
const update = buildStatusUpdate(status);
|
|
130
|
+
const { error } = await supabase
|
|
131
|
+
.from('opportunities')
|
|
132
|
+
.update(update)
|
|
133
|
+
.eq('id', id);
|
|
134
|
+
if (error)
|
|
135
|
+
throw new Error(`Failed to update status: ${error.message}`);
|
|
136
|
+
}
|
|
137
|
+
export async function updateOpportunityNotes(supabase, id, notes) {
|
|
138
|
+
const { error } = await supabase
|
|
139
|
+
.from('opportunities')
|
|
140
|
+
.update({ notes })
|
|
141
|
+
.eq('id', id);
|
|
142
|
+
if (error)
|
|
143
|
+
throw new Error(`Failed to update notes: ${error.message}`);
|
|
144
|
+
}
|
|
145
|
+
export async function getOpportunities(supabase, options = {}) {
|
|
146
|
+
let query = supabase
|
|
147
|
+
.from('opportunities')
|
|
148
|
+
.select('*')
|
|
149
|
+
.order('score', { ascending: false, nullsFirst: false });
|
|
150
|
+
if (options.track)
|
|
151
|
+
query = query.eq('track', options.track);
|
|
152
|
+
if (options.status)
|
|
153
|
+
query = query.eq('status', options.status);
|
|
154
|
+
if (options.minScore)
|
|
155
|
+
query = query.gte('score', options.minScore);
|
|
156
|
+
if (options.limit)
|
|
157
|
+
query = query.limit(options.limit);
|
|
158
|
+
if (options.days) {
|
|
159
|
+
const since = new Date();
|
|
160
|
+
since.setDate(since.getDate() - options.days);
|
|
161
|
+
query = query.gte('created_at', since.toISOString());
|
|
162
|
+
}
|
|
163
|
+
const { data, error } = await query;
|
|
164
|
+
if (error)
|
|
165
|
+
throw new Error(`Failed to fetch opportunities: ${error.message}`);
|
|
166
|
+
return (data ?? []);
|
|
167
|
+
}
|
|
168
|
+
export async function getOpportunityById(supabase, id) {
|
|
169
|
+
const { data, error } = await supabase
|
|
170
|
+
.from('opportunities')
|
|
171
|
+
.select('*')
|
|
172
|
+
.eq('id', id)
|
|
173
|
+
.maybeSingle();
|
|
174
|
+
if (error)
|
|
175
|
+
throw new Error(`Failed to fetch opportunity: ${error.message}`);
|
|
176
|
+
return data;
|
|
177
|
+
}
|
|
178
|
+
export async function getExistingUrls(supabase, urls) {
|
|
179
|
+
const existing = new Set();
|
|
180
|
+
// Batch to avoid oversized IN queries
|
|
181
|
+
const batchSize = 50;
|
|
182
|
+
for (let i = 0; i < urls.length; i += batchSize) {
|
|
183
|
+
const batch = urls.slice(i, i + batchSize);
|
|
184
|
+
const { data, error } = await supabase
|
|
185
|
+
.from('opportunities')
|
|
186
|
+
.select('url')
|
|
187
|
+
.in('url', batch);
|
|
188
|
+
if (error)
|
|
189
|
+
throw new Error(`Failed to check existing URLs: ${error.message}`);
|
|
190
|
+
for (const row of data ?? [])
|
|
191
|
+
existing.add(row.url);
|
|
192
|
+
}
|
|
193
|
+
return existing;
|
|
194
|
+
}
|
|
195
|
+
export async function getOpportunitiesForPeriod(supabase, start, end) {
|
|
196
|
+
const { data, error } = await supabase
|
|
197
|
+
.from('opportunities')
|
|
198
|
+
.select('*')
|
|
199
|
+
.gte('created_at', start)
|
|
200
|
+
.lte('created_at', end);
|
|
201
|
+
if (error)
|
|
202
|
+
throw new Error(`Failed to fetch period: ${error.message}`);
|
|
203
|
+
return (data ?? []);
|
|
204
|
+
}
|
|
205
|
+
export async function insertAnalytics(supabase, row) {
|
|
206
|
+
const { error } = await supabase
|
|
207
|
+
.from('hunt_analytics')
|
|
208
|
+
.upsert(row, { onConflict: 'period_start,period_end' });
|
|
209
|
+
if (error)
|
|
210
|
+
throw new Error(`Failed to insert analytics: ${error.message}`);
|
|
211
|
+
}
|
|
212
|
+
export async function getLatestAnalytics(supabase) {
|
|
213
|
+
const { data, error } = await supabase
|
|
214
|
+
.from('hunt_analytics')
|
|
215
|
+
.select('*')
|
|
216
|
+
.order('period_end', { ascending: false })
|
|
217
|
+
.limit(1)
|
|
218
|
+
.maybeSingle();
|
|
219
|
+
if (error)
|
|
220
|
+
throw new Error(`Failed to fetch analytics: ${error.message}`);
|
|
221
|
+
return data;
|
|
222
|
+
}
|
|
223
|
+
export async function purgeOldRejected(supabase, days) {
|
|
224
|
+
const cutoff = new Date();
|
|
225
|
+
cutoff.setDate(cutoff.getDate() - days);
|
|
226
|
+
const { data, error } = await supabase
|
|
227
|
+
.from('opportunities')
|
|
228
|
+
.delete()
|
|
229
|
+
.eq('status', 'rejected')
|
|
230
|
+
.lt('created_at', cutoff.toISOString())
|
|
231
|
+
.select('id');
|
|
232
|
+
if (error)
|
|
233
|
+
throw new Error(`Failed to purge: ${error.message}`);
|
|
234
|
+
return data?.length ?? 0;
|
|
235
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import Parser from 'rss-parser';
|
|
2
|
+
const parser = new Parser();
|
|
3
|
+
export function parseUpworkItem(item, sourceName) {
|
|
4
|
+
return {
|
|
5
|
+
url: item.link ?? '',
|
|
6
|
+
title: item.title ?? '',
|
|
7
|
+
description: item.contentSnippet || item.content || '',
|
|
8
|
+
source: sourceName,
|
|
9
|
+
};
|
|
10
|
+
}
|
|
11
|
+
export function deduplicateListings(listings, existingUrls) {
|
|
12
|
+
return listings.filter(l => !existingUrls.has(l.url));
|
|
13
|
+
}
|
|
14
|
+
export async function fetchFeed(feed) {
|
|
15
|
+
const result = await parser.parseURL(feed.url);
|
|
16
|
+
return (result.items ?? []).map(item => parseUpworkItem(item, feed.name));
|
|
17
|
+
}
|
|
18
|
+
export async function fetchAllFeeds(feeds) {
|
|
19
|
+
const all = [];
|
|
20
|
+
for (const feed of feeds) {
|
|
21
|
+
try {
|
|
22
|
+
const listings = await fetchFeed(feed);
|
|
23
|
+
all.push(...listings);
|
|
24
|
+
}
|
|
25
|
+
catch (err) {
|
|
26
|
+
console.error(`Failed to fetch feed "${feed.name}": ${err.message}`);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return all;
|
|
30
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { resolve, dirname } from 'path';
|
|
3
|
+
import { fileURLToPath } from 'url';
|
|
4
|
+
import { isValidTrack, isValidRecommendation } from './hunter-types.js';
|
|
5
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
6
|
+
export function loadSystemPrompt() {
|
|
7
|
+
const promptPath = resolve(__dirname, '../prompts/hunter-system.txt');
|
|
8
|
+
return readFileSync(promptPath, 'utf-8');
|
|
9
|
+
}
|
|
10
|
+
export function buildScoringPrompt(opportunityText) {
|
|
11
|
+
return `Score this opportunity:\n\n<opportunity>\n${opportunityText}\n</opportunity>`;
|
|
12
|
+
}
|
|
13
|
+
export function parseScoringResponse(response) {
|
|
14
|
+
let cleaned = response.trim();
|
|
15
|
+
const fenceMatch = cleaned.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/);
|
|
16
|
+
if (fenceMatch) {
|
|
17
|
+
cleaned = fenceMatch[1].trim();
|
|
18
|
+
}
|
|
19
|
+
let parsed;
|
|
20
|
+
try {
|
|
21
|
+
parsed = JSON.parse(cleaned);
|
|
22
|
+
}
|
|
23
|
+
catch {
|
|
24
|
+
throw new Error(`Hunter returned invalid JSON: ${cleaned.slice(0, 200)}`);
|
|
25
|
+
}
|
|
26
|
+
const obj = parsed;
|
|
27
|
+
if (!obj.track || !isValidTrack(obj.track)) {
|
|
28
|
+
throw new Error(`Invalid or missing track: ${obj.track}`);
|
|
29
|
+
}
|
|
30
|
+
if (typeof obj.score !== 'number') {
|
|
31
|
+
throw new Error(`Invalid or missing score: ${obj.score}`);
|
|
32
|
+
}
|
|
33
|
+
if (!obj.recommendation || !isValidRecommendation(obj.recommendation)) {
|
|
34
|
+
throw new Error(`Invalid or missing recommendation: ${obj.recommendation}`);
|
|
35
|
+
}
|
|
36
|
+
if (typeof obj.summary !== 'string') {
|
|
37
|
+
throw new Error(`Invalid or missing summary`);
|
|
38
|
+
}
|
|
39
|
+
if (!Array.isArray(obj.key_requirements)) {
|
|
40
|
+
throw new Error(`Invalid or missing key_requirements`);
|
|
41
|
+
}
|
|
42
|
+
if (!obj.score_breakdown || typeof obj.score_breakdown !== 'object') {
|
|
43
|
+
throw new Error(`Invalid or missing score_breakdown`);
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
track: obj.track,
|
|
47
|
+
score: obj.score,
|
|
48
|
+
recommendation: obj.recommendation,
|
|
49
|
+
summary: obj.summary,
|
|
50
|
+
key_requirements: obj.key_requirements,
|
|
51
|
+
compensation_range: obj.compensation_range ?? null,
|
|
52
|
+
reject_reasons: obj.reject_reasons ?? null,
|
|
53
|
+
score_breakdown: obj.score_breakdown,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// --- Constants ---
|
|
2
|
+
export const TRACKS = ['freelance', 'employment'];
|
|
3
|
+
export const STATUSES = [
|
|
4
|
+
'new', 'reviewed', 'applied', 'interviewing', 'won', 'lost', 'rejected',
|
|
5
|
+
];
|
|
6
|
+
export const RECOMMENDATIONS = ['apply', 'ask', 'skip'];
|
|
7
|
+
export const REJECT_REASONS = [
|
|
8
|
+
'BUDGET_TOO_LOW',
|
|
9
|
+
'SCOPE_UNCLEAR',
|
|
10
|
+
'TIMELINE_UNREALISTIC',
|
|
11
|
+
'CLIENT_RED_FLAGS',
|
|
12
|
+
'SKILL_MISMATCH',
|
|
13
|
+
'PLATFORM_RISK',
|
|
14
|
+
'COMPLEXITY_EXCESSIVE',
|
|
15
|
+
'LOW_STRATEGIC_VALUE',
|
|
16
|
+
'SALARY_BELOW_MARKET',
|
|
17
|
+
'NO_REMOTE',
|
|
18
|
+
'STACK_MISMATCH',
|
|
19
|
+
];
|
|
20
|
+
// --- Validators ---
|
|
21
|
+
export function isValidTrack(value) {
|
|
22
|
+
return TRACKS.includes(value);
|
|
23
|
+
}
|
|
24
|
+
export function isValidStatus(value) {
|
|
25
|
+
return STATUSES.includes(value);
|
|
26
|
+
}
|
|
27
|
+
export function isValidRecommendation(value) {
|
|
28
|
+
return RECOMMENDATIONS.includes(value);
|
|
29
|
+
}
|
|
30
|
+
export const DEFAULT_HUNTER_CONFIG = {
|
|
31
|
+
feeds: [],
|
|
32
|
+
cron_schedule: '*/30 8-23 * * *',
|
|
33
|
+
analytics_interval_days: 15,
|
|
34
|
+
purge_after_days: 90,
|
|
35
|
+
min_score_highlight: 80,
|
|
36
|
+
};
|
package/dist/lib/lint-configs.js
CHANGED
|
@@ -61,6 +61,26 @@ export default [
|
|
|
61
61
|
{
|
|
62
62
|
rules: {
|
|
63
63
|
'no-warning-comments': ['warn', { terms: ['TODO', 'FIXME', 'HACK'] }],
|
|
64
|
+
'@typescript-eslint/naming-convention': [
|
|
65
|
+
'error',
|
|
66
|
+
{
|
|
67
|
+
selector: 'interface',
|
|
68
|
+
format: ['PascalCase'],
|
|
69
|
+
prefix: ['I'],
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
selector: 'variable',
|
|
73
|
+
format: ['camelCase', 'UPPER_CASE', 'PascalCase'],
|
|
74
|
+
filter: { regex: '^_$', match: false },
|
|
75
|
+
custom: { regex: '^[a-zA-Z]$', match: false },
|
|
76
|
+
},
|
|
77
|
+
{
|
|
78
|
+
selector: 'parameter',
|
|
79
|
+
format: ['camelCase'],
|
|
80
|
+
filter: { regex: '^_', match: false },
|
|
81
|
+
custom: { regex: '^[a-zA-Z]$', match: false },
|
|
82
|
+
},
|
|
83
|
+
],
|
|
64
84
|
},
|
|
65
85
|
},
|
|
66
86
|
];
|
package/dist/lib/migrate.js
CHANGED
|
@@ -5,7 +5,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
5
5
|
const MIGRATIONS_DIR = resolve(__dirname, '../migrations');
|
|
6
6
|
export function getMigrationFiles() {
|
|
7
7
|
return readdirSync(MIGRATIONS_DIR)
|
|
8
|
-
.filter(
|
|
8
|
+
.filter(file => file.endsWith('.sql'))
|
|
9
9
|
.sort();
|
|
10
10
|
}
|
|
11
11
|
export function readMigration(filename) {
|
|
@@ -19,5 +19,5 @@ export async function getAppliedMigrations(supabase) {
|
|
|
19
19
|
// Table doesn't exist yet — no migrations applied
|
|
20
20
|
return new Set();
|
|
21
21
|
}
|
|
22
|
-
return new Set((data || []).map(
|
|
22
|
+
return new Set((data || []).map(row => row.version));
|
|
23
23
|
}
|