job_ops-mcp 0.3.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.
Files changed (116) hide show
  1. package/.env.example +33 -0
  2. package/LICENSE +21 -0
  3. package/README.md +400 -0
  4. package/config/profile.example.yml +67 -0
  5. package/cv.example.md +53 -0
  6. package/dist/cli.js +385 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/config.js +63 -0
  9. package/dist/config.js.map +1 -0
  10. package/dist/core/browser.js +27 -0
  11. package/dist/core/browser.js.map +1 -0
  12. package/dist/core/content_hash.js +11 -0
  13. package/dist/core/content_hash.js.map +1 -0
  14. package/dist/core/csv.js +107 -0
  15. package/dist/core/csv.js.map +1 -0
  16. package/dist/core/cv_parse.js +201 -0
  17. package/dist/core/cv_parse.js.map +1 -0
  18. package/dist/core/html.js +10 -0
  19. package/dist/core/html.js.map +1 -0
  20. package/dist/core/jd_normalize.js +99 -0
  21. package/dist/core/jd_normalize.js.map +1 -0
  22. package/dist/core/jobs.js +106 -0
  23. package/dist/core/jobs.js.map +1 -0
  24. package/dist/core/llm.js +227 -0
  25. package/dist/core/llm.js.map +1 -0
  26. package/dist/core/modes.js +55 -0
  27. package/dist/core/modes.js.map +1 -0
  28. package/dist/core/outreach_safety.js +77 -0
  29. package/dist/core/outreach_safety.js.map +1 -0
  30. package/dist/core/profile.js +88 -0
  31. package/dist/core/profile.js.map +1 -0
  32. package/dist/core/providers/amazon.js +36 -0
  33. package/dist/core/providers/amazon.js.map +1 -0
  34. package/dist/core/providers/ashby.js +31 -0
  35. package/dist/core/providers/ashby.js.map +1 -0
  36. package/dist/core/providers/google.js +46 -0
  37. package/dist/core/providers/google.js.map +1 -0
  38. package/dist/core/providers/greenhouse.js +55 -0
  39. package/dist/core/providers/greenhouse.js.map +1 -0
  40. package/dist/core/providers/http.js +36 -0
  41. package/dist/core/providers/http.js.map +1 -0
  42. package/dist/core/providers/index.js +53 -0
  43. package/dist/core/providers/index.js.map +1 -0
  44. package/dist/core/providers/lever.js +32 -0
  45. package/dist/core/providers/lever.js.map +1 -0
  46. package/dist/core/providers/playwright_generic.js +53 -0
  47. package/dist/core/providers/playwright_generic.js.map +1 -0
  48. package/dist/core/providers/types.js +2 -0
  49. package/dist/core/providers/types.js.map +1 -0
  50. package/dist/core/providers/workday.js +44 -0
  51. package/dist/core/providers/workday.js.map +1 -0
  52. package/dist/core/render.js +253 -0
  53. package/dist/core/render.js.map +1 -0
  54. package/dist/core/reports.js +257 -0
  55. package/dist/core/reports.js.map +1 -0
  56. package/dist/core/resources.js +40 -0
  57. package/dist/core/resources.js.map +1 -0
  58. package/dist/core/scan_engine.js +164 -0
  59. package/dist/core/scan_engine.js.map +1 -0
  60. package/dist/core/scheduler.js +117 -0
  61. package/dist/core/scheduler.js.map +1 -0
  62. package/dist/db.js +60 -0
  63. package/dist/db.js.map +1 -0
  64. package/dist/http/app.js +35 -0
  65. package/dist/http/app.js.map +1 -0
  66. package/dist/http/dashboard.js +131 -0
  67. package/dist/http/dashboard.js.map +1 -0
  68. package/dist/mcp/define.js +35 -0
  69. package/dist/mcp/define.js.map +1 -0
  70. package/dist/mcp/server.js +103 -0
  71. package/dist/mcp/server.js.map +1 -0
  72. package/dist/mcp/tools/apply_prefill.js +167 -0
  73. package/dist/mcp/tools/apply_prefill.js.map +1 -0
  74. package/dist/mcp/tools/batch_evaluate.js +143 -0
  75. package/dist/mcp/tools/batch_evaluate.js.map +1 -0
  76. package/dist/mcp/tools/evaluate_job.js +181 -0
  77. package/dist/mcp/tools/evaluate_job.js.map +1 -0
  78. package/dist/mcp/tools/generate_materials.js +126 -0
  79. package/dist/mcp/tools/generate_materials.js.map +1 -0
  80. package/dist/mcp/tools/get_report.js +24 -0
  81. package/dist/mcp/tools/get_report.js.map +1 -0
  82. package/dist/mcp/tools/ops.js +321 -0
  83. package/dist/mcp/tools/ops.js.map +1 -0
  84. package/dist/mcp/tools/outreach.js +481 -0
  85. package/dist/mcp/tools/outreach.js.map +1 -0
  86. package/dist/mcp/tools/render_pdf.js +27 -0
  87. package/dist/mcp/tools/render_pdf.js.map +1 -0
  88. package/dist/mcp/tools/scan_portals.js +35 -0
  89. package/dist/mcp/tools/scan_portals.js.map +1 -0
  90. package/dist/mcp/tools/scheduler.js +32 -0
  91. package/dist/mcp/tools/scheduler.js.map +1 -0
  92. package/dist/mcp/tools/stories.js +172 -0
  93. package/dist/mcp/tools/stories.js.map +1 -0
  94. package/dist/mcp/tools/tracker.js +183 -0
  95. package/dist/mcp/tools/tracker.js.map +1 -0
  96. package/dist/mcp/tools/visa.js +219 -0
  97. package/dist/mcp/tools/visa.js.map +1 -0
  98. package/dist/migrations/001_initial.sql +505 -0
  99. package/dist/migrations/002_llm_and_digest.sql +42 -0
  100. package/dist/server.js +55 -0
  101. package/dist/server.js.map +1 -0
  102. package/fonts/dm-sans-latin-ext.woff2 +0 -0
  103. package/fonts/dm-sans-latin.woff2 +0 -0
  104. package/fonts/space-grotesk-latin-ext.woff2 +0 -0
  105. package/fonts/space-grotesk-latin.woff2 +0 -0
  106. package/modes/career_packet.md +91 -0
  107. package/modes/negotiation_playbook.md +64 -0
  108. package/modes/outreach_tone.md +80 -0
  109. package/modes/report_format.md +83 -0
  110. package/modes/rubric.md +119 -0
  111. package/modes/tailoring_rules.md +102 -0
  112. package/package.json +67 -0
  113. package/portals.example.yml +95 -0
  114. package/templates/cover-template.html +64 -0
  115. package/templates/cv-template.html +421 -0
  116. package/templates/cv-template.tex +123 -0
@@ -0,0 +1,505 @@
1
+ -- mcp-jsa initial schema.
2
+ -- Collapses the JSA Postgres schema (study guide §3) + career-ops side-tables (eval_reports,
3
+ -- story_bank, negotiation_notes) into a single SQLite file. Shapes match JSA columns so SQL
4
+ -- recipes from the study guide (§10, §12.9) port over with minimal edits.
5
+ --
6
+ -- Conventions:
7
+ -- - id: TEXT, lowercase UUIDv4 generated in app code via crypto.randomUUID()
8
+ -- - timestamps: TEXT, ISO-8601 with timezone (CURRENT_TIMESTAMP yields 'YYYY-MM-DD HH:MM:SS')
9
+ -- - JSONB → TEXT (parsed in app); accessed via SQLite json_*() functions for views
10
+ -- - booleans → INTEGER 0/1
11
+ -- Status columns use CHECK constraints (canonical states from the brief + JSA + career-ops).
12
+
13
+ PRAGMA foreign_keys = ON;
14
+
15
+ -- ──────────────────────────────────────────────────────────────────────────────
16
+ -- companies
17
+ -- ──────────────────────────────────────────────────────────────────────────────
18
+ CREATE TABLE companies (
19
+ id TEXT PRIMARY KEY,
20
+ name TEXT NOT NULL,
21
+ name_normalized TEXT NOT NULL UNIQUE,
22
+ website TEXT,
23
+ linkedin_url TEXT,
24
+ hq_city TEXT,
25
+ hq_country TEXT,
26
+ headcount_range TEXT,
27
+ funding_stage TEXT,
28
+ notes TEXT,
29
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
30
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
31
+ );
32
+
33
+ CREATE INDEX idx_companies_name_normalized ON companies(name_normalized);
34
+
35
+ -- ──────────────────────────────────────────────────────────────────────────────
36
+ -- company_aliases — alternate spellings (LinkedIn / DOL / etc.)
37
+ -- ──────────────────────────────────────────────────────────────────────────────
38
+ CREATE TABLE company_aliases (
39
+ id TEXT PRIMARY KEY,
40
+ company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
41
+ alias TEXT NOT NULL,
42
+ alias_normalized TEXT NOT NULL,
43
+ source TEXT,
44
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
45
+ UNIQUE (alias_normalized, source)
46
+ );
47
+
48
+ CREATE INDEX idx_company_aliases_alias_norm ON company_aliases(alias_normalized);
49
+ CREATE INDEX idx_company_aliases_company_id ON company_aliases(company_id);
50
+
51
+ -- ──────────────────────────────────────────────────────────────────────────────
52
+ -- target_companies — what the pollers actually scrape
53
+ -- ──────────────────────────────────────────────────────────────────────────────
54
+ CREATE TABLE target_companies (
55
+ id TEXT PRIMARY KEY,
56
+ company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
57
+ greenhouse_slug TEXT,
58
+ ashby_slug TEXT,
59
+ lever_slug TEXT,
60
+ workday_url TEXT,
61
+ careers_url TEXT,
62
+ priority INTEGER NOT NULL DEFAULT 2,
63
+ is_active INTEGER NOT NULL DEFAULT 1,
64
+ last_polled_at TEXT,
65
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
66
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
67
+ );
68
+
69
+ CREATE INDEX idx_target_companies_active ON target_companies(is_active);
70
+
71
+ -- ──────────────────────────────────────────────────────────────────────────────
72
+ -- jobs — main table; status state machine matches the brief exactly
73
+ -- ──────────────────────────────────────────────────────────────────────────────
74
+ CREATE TABLE jobs (
75
+ id TEXT PRIMARY KEY,
76
+ source TEXT NOT NULL, -- greenhouse | ashby | lever | workday | manual | paste | ...
77
+ source_job_id TEXT,
78
+ source_url TEXT NOT NULL,
79
+ content_hash TEXT UNIQUE,
80
+ company_id TEXT REFERENCES companies(id),
81
+ company_name_raw TEXT NOT NULL,
82
+ title TEXT NOT NULL,
83
+ role_category TEXT, -- pm | ml_eng | data_eng | analytics_eng | swe | forward_deployed | other
84
+ seniority TEXT, -- intern | junior | mid | senior | staff | principal | lead | unclear
85
+ location_raw TEXT,
86
+ location_city TEXT,
87
+ location_region TEXT,
88
+ location_country TEXT,
89
+ is_remote INTEGER,
90
+ is_hybrid INTEGER,
91
+ comp_min_usd INTEGER,
92
+ comp_max_usd INTEGER,
93
+ comp_currency TEXT DEFAULT 'USD',
94
+ description TEXT,
95
+ description_html TEXT,
96
+ requirements TEXT,
97
+ sponsors_visa INTEGER,
98
+ visa_signal_source TEXT,
99
+ visa_notes TEXT,
100
+ status TEXT NOT NULL DEFAULT 'sourced'
101
+ CHECK (status IN (
102
+ 'sourced','ready_to_apply','materials_drafted','ready_to_review',
103
+ 'applied','screen','onsite','offer','rejected','discarded','skip'
104
+ )),
105
+ declared_archetype TEXT, -- optional user override of role_category for scoring
106
+ score_total INTEGER, -- 0..100
107
+ score_resume_fit INTEGER,
108
+ score_taste_fit INTEGER,
109
+ score_visa_fit INTEGER,
110
+ score_detail TEXT, -- JSON: reasoning, concerns, parse_error, raw
111
+ scored_at TEXT,
112
+ materials_generated_at TEXT,
113
+ applied_at TEXT,
114
+ posted_at TEXT,
115
+ discovered_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
116
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
117
+ UNIQUE (source, source_job_id)
118
+ );
119
+
120
+ CREATE INDEX idx_jobs_company_id ON jobs(company_id);
121
+ CREATE INDEX idx_jobs_status ON jobs(status);
122
+ CREATE INDEX idx_jobs_score_total ON jobs(score_total DESC);
123
+ CREATE INDEX idx_jobs_discovered_at ON jobs(discovered_at DESC);
124
+ CREATE INDEX idx_jobs_role_category ON jobs(role_category);
125
+ CREATE INDEX idx_jobs_location_ctry ON jobs(location_country);
126
+
127
+ -- ──────────────────────────────────────────────────────────────────────────────
128
+ -- linkedin_connections — imported network
129
+ -- ──────────────────────────────────────────────────────────────────────────────
130
+ CREATE TABLE linkedin_connections (
131
+ id TEXT PRIMARY KEY,
132
+ first_name TEXT,
133
+ last_name TEXT,
134
+ full_name TEXT,
135
+ email TEXT,
136
+ linkedin_url TEXT UNIQUE,
137
+ twitter_url TEXT,
138
+ company_id TEXT REFERENCES companies(id),
139
+ company_raw TEXT,
140
+ position TEXT,
141
+ seniority_inferred TEXT,
142
+ is_recruiter INTEGER NOT NULL DEFAULT 0,
143
+ is_engineering INTEGER NOT NULL DEFAULT 0,
144
+ is_leadership INTEGER NOT NULL DEFAULT 0,
145
+ preferred_channel TEXT DEFAULT 'linkedin',
146
+ outreach_notes TEXT,
147
+ notes TEXT,
148
+ connected_on TEXT,
149
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
150
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
151
+ );
152
+
153
+ CREATE INDEX idx_li_company_id ON linkedin_connections(company_id);
154
+ CREATE INDEX idx_li_full_name ON linkedin_connections(full_name);
155
+ CREATE INDEX idx_li_position ON linkedin_connections(position);
156
+ CREATE INDEX idx_li_is_recruiter ON linkedin_connections(is_recruiter);
157
+
158
+ -- ──────────────────────────────────────────────────────────────────────────────
159
+ -- h1b_filings — DOL OFLC quarterly data
160
+ -- ──────────────────────────────────────────────────────────────────────────────
161
+ CREATE TABLE h1b_filings (
162
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
163
+ case_number TEXT NOT NULL UNIQUE,
164
+ case_status TEXT NOT NULL,
165
+ visa_class TEXT,
166
+ employer_id TEXT REFERENCES companies(id),
167
+ employer_name_raw TEXT NOT NULL,
168
+ job_title TEXT,
169
+ soc_code TEXT,
170
+ soc_title TEXT,
171
+ work_city TEXT,
172
+ work_state TEXT,
173
+ work_postal_code TEXT,
174
+ wage_rate_from REAL,
175
+ wage_rate_to REAL,
176
+ wage_unit TEXT,
177
+ prevailing_wage REAL,
178
+ received_date TEXT,
179
+ decision_date TEXT,
180
+ employment_start TEXT,
181
+ employment_end TEXT,
182
+ full_time INTEGER,
183
+ new_employment INTEGER,
184
+ fiscal_year INTEGER NOT NULL,
185
+ raw_json TEXT,
186
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
187
+ );
188
+
189
+ CREATE INDEX idx_h1b_employer_id ON h1b_filings(employer_id);
190
+ CREATE INDEX idx_h1b_employer_raw ON h1b_filings(employer_name_raw);
191
+ CREATE INDEX idx_h1b_soc_code ON h1b_filings(soc_code);
192
+ CREATE INDEX idx_h1b_decision_date ON h1b_filings(decision_date DESC);
193
+ CREATE INDEX idx_h1b_fiscal_year ON h1b_filings(fiscal_year);
194
+ CREATE INDEX idx_h1b_case_status ON h1b_filings(case_status);
195
+
196
+ -- ──────────────────────────────────────────────────────────────────────────────
197
+ -- outreach — every warm-intro / founder DM lives here
198
+ -- ──────────────────────────────────────────────────────────────────────────────
199
+ CREATE TABLE outreach (
200
+ id TEXT PRIMARY KEY,
201
+ connection_id TEXT REFERENCES linkedin_connections(id) ON DELETE SET NULL,
202
+ company_id TEXT REFERENCES companies(id),
203
+ related_job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
204
+ outreach_type TEXT NOT NULL
205
+ CHECK (outreach_type IN ('warm_intro_request','founder_dm','recruiter_followup','generic','followup')),
206
+ channel TEXT NOT NULL DEFAULT 'linkedin',
207
+ status TEXT NOT NULL DEFAULT 'queued'
208
+ CHECK (status IN ('queued','drafted','edited','sent','replied','dead','success')),
209
+ draft_message TEXT,
210
+ edited_message TEXT,
211
+ subject_line TEXT,
212
+ reply_text TEXT,
213
+ notes TEXT,
214
+ sent_at TEXT,
215
+ replied_at TEXT,
216
+ followup_due_at TEXT,
217
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
218
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
219
+ );
220
+
221
+ CREATE INDEX idx_outreach_status ON outreach(status);
222
+ CREATE INDEX idx_outreach_connection ON outreach(connection_id);
223
+ CREATE INDEX idx_outreach_followup_due ON outreach(followup_due_at);
224
+ CREATE INDEX idx_outreach_company ON outreach(company_id);
225
+
226
+ -- ──────────────────────────────────────────────────────────────────────────────
227
+ -- applications — one per (job × materials version)
228
+ -- ──────────────────────────────────────────────────────────────────────────────
229
+ CREATE TABLE applications (
230
+ id TEXT PRIMARY KEY,
231
+ job_id TEXT NOT NULL UNIQUE REFERENCES jobs(id) ON DELETE CASCADE,
232
+ status TEXT NOT NULL DEFAULT 'materials_drafted'
233
+ CHECK (status IN (
234
+ 'materials_drafted','render_error','ready_to_review','applied','screen',
235
+ 'onsite','offer','rejected','discarded'
236
+ )),
237
+ tailored_bullets TEXT, -- JSON: { tagline, experience_bullets: {<employer_slug>: [...]}, projects_section, skills_section }
238
+ cover_letter_draft TEXT, -- plain prose, 250-350 words
239
+ tailoring_notes TEXT,
240
+ resume_path TEXT, -- path under output/ (returned as localhost link)
241
+ cover_path TEXT,
242
+ resume_tex TEXT,
243
+ cover_letter_tex TEXT,
244
+ materials_v INTEGER NOT NULL DEFAULT 1,
245
+ last_status_change_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
246
+ notes TEXT,
247
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
248
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
249
+ );
250
+
251
+ -- ──────────────────────────────────────────────────────────────────────────────
252
+ -- enrichment — Brave / web research summaries with 30-day TTL
253
+ -- ──────────────────────────────────────────────────────────────────────────────
254
+ CREATE TABLE enrichment (
255
+ id TEXT PRIMARY KEY,
256
+ company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
257
+ kind TEXT NOT NULL CHECK (kind IN ('comp','culture','recent_news')),
258
+ raw_search_results TEXT, -- JSON
259
+ summary TEXT,
260
+ confidence_score INTEGER, -- 0..100
261
+ signal_quality TEXT, -- strong | mixed | weak | none
262
+ flags TEXT,
263
+ source_urls TEXT, -- JSON array
264
+ expires_at TEXT NOT NULL,
265
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
266
+ UNIQUE (company_id, kind)
267
+ );
268
+
269
+ CREATE INDEX idx_enrichment_expires_at ON enrichment(expires_at);
270
+
271
+ -- ──────────────────────────────────────────────────────────────────────────────
272
+ -- career_packet — single is_active row; full version history retained
273
+ -- ──────────────────────────────────────────────────────────────────────────────
274
+ CREATE TABLE career_packet (
275
+ id TEXT PRIMARY KEY,
276
+ version INTEGER NOT NULL,
277
+ content TEXT NOT NULL, -- markdown source-of-truth packet
278
+ taglines TEXT, -- JSON array of alternates
279
+ is_active INTEGER NOT NULL DEFAULT 0,
280
+ source_cv_hash TEXT,
281
+ notes TEXT,
282
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
283
+ );
284
+
285
+ CREATE INDEX idx_career_packet_active ON career_packet(is_active);
286
+
287
+ -- ──────────────────────────────────────────────────────────────────────────────
288
+ -- contacts — company-level contact discovery (people without LinkedIn rows)
289
+ -- ──────────────────────────────────────────────────────────────────────────────
290
+ CREATE TABLE contacts (
291
+ id TEXT PRIMARY KEY,
292
+ company_id TEXT NOT NULL REFERENCES companies(id) ON DELETE CASCADE,
293
+ full_name TEXT,
294
+ email TEXT,
295
+ role TEXT,
296
+ source TEXT,
297
+ notes TEXT,
298
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
299
+ );
300
+
301
+ -- ──────────────────────────────────────────────────────────────────────────────
302
+ -- eval_reports — career-ops 6-block report (A–F) persisted; G is in score_detail
303
+ -- ──────────────────────────────────────────────────────────────────────────────
304
+ CREATE TABLE eval_reports (
305
+ id TEXT PRIMARY KEY,
306
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
307
+ mode TEXT NOT NULL CHECK (mode IN ('chat','api')),
308
+ archetype_detected TEXT,
309
+ block_role_summary TEXT, -- A
310
+ block_cv_match TEXT, -- B
311
+ block_level TEXT, -- C
312
+ block_comp TEXT, -- D
313
+ block_personalize TEXT, -- E
314
+ block_interview TEXT, -- F
315
+ block_legitimacy TEXT, -- G (career-ops) — optional, populated when chat fills it
316
+ keywords TEXT, -- JSON array
317
+ raw_input TEXT, -- normalized JD text passed to chat
318
+ html_path TEXT, -- under output/, served via /files/
319
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
320
+ );
321
+
322
+ CREATE INDEX idx_eval_reports_job ON eval_reports(job_id);
323
+
324
+ -- ──────────────────────────────────────────────────────────────────────────────
325
+ -- story_bank — STAR + Reflection stories distilled across evaluations
326
+ -- ──────────────────────────────────────────────────────────────────────────────
327
+ CREATE TABLE story_bank (
328
+ id TEXT PRIMARY KEY,
329
+ job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
330
+ story_text TEXT NOT NULL,
331
+ reflection TEXT,
332
+ competency_tags TEXT, -- JSON array
333
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
334
+ );
335
+
336
+ CREATE INDEX idx_story_bank_job ON story_bank(job_id);
337
+
338
+ -- ──────────────────────────────────────────────────────────────────────────────
339
+ -- negotiation_notes — per-offer negotiation worksheet
340
+ -- ──────────────────────────────────────────────────────────────────────────────
341
+ CREATE TABLE negotiation_notes (
342
+ id TEXT PRIMARY KEY,
343
+ job_id TEXT NOT NULL REFERENCES jobs(id) ON DELETE CASCADE,
344
+ framework TEXT,
345
+ leverage TEXT,
346
+ geo_pushback TEXT,
347
+ comp_target TEXT,
348
+ notes TEXT,
349
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
350
+ updated_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
351
+ );
352
+
353
+ -- ──────────────────────────────────────────────────────────────────────────────
354
+ -- scheduler_state — single-row table for opt-in cron job enable/disable
355
+ -- ──────────────────────────────────────────────────────────────────────────────
356
+ CREATE TABLE scheduler_state (
357
+ id INTEGER PRIMARY KEY CHECK (id = 1),
358
+ enabled_jobs TEXT NOT NULL DEFAULT '[]', -- JSON array of job names
359
+ last_run_at TEXT,
360
+ notes TEXT
361
+ );
362
+ INSERT INTO scheduler_state (id, enabled_jobs) VALUES (1, '[]');
363
+
364
+ -- ──────────────────────────────────────────────────────────────────────────────
365
+ -- Views — JSA views recreated against SQLite (study guide §3.3)
366
+ -- ──────────────────────────────────────────────────────────────────────────────
367
+
368
+ CREATE VIEW v_rated_jobs AS
369
+ SELECT
370
+ j.id AS job_id,
371
+ j.title,
372
+ c.name AS company_name,
373
+ j.location_raw AS location,
374
+ j.role_category,
375
+ j.seniority,
376
+ j.score_total,
377
+ j.score_resume_fit,
378
+ j.score_taste_fit,
379
+ j.score_visa_fit,
380
+ j.status,
381
+ j.source_url,
382
+ j.discovered_at,
383
+ j.scored_at
384
+ FROM jobs j
385
+ LEFT JOIN companies c ON c.id = j.company_id
386
+ WHERE j.score_total IS NOT NULL;
387
+
388
+ CREATE VIEW v_top_jobs AS
389
+ SELECT * FROM v_rated_jobs
390
+ WHERE score_total >= 75
391
+ ORDER BY score_total DESC, discovered_at DESC;
392
+
393
+ CREATE VIEW v_apply_ready AS
394
+ SELECT
395
+ j.id AS job_id,
396
+ j.title,
397
+ c.name AS company_name,
398
+ j.score_total,
399
+ j.role_category,
400
+ j.location_raw AS location,
401
+ j.source_url,
402
+ j.status,
403
+ a.id AS application_id,
404
+ a.resume_path,
405
+ a.cover_path,
406
+ a.materials_v,
407
+ a.last_status_change_at
408
+ FROM jobs j
409
+ LEFT JOIN companies c ON c.id = j.company_id
410
+ LEFT JOIN applications a ON a.job_id = j.id
411
+ WHERE j.status IN ('ready_to_apply','materials_drafted','ready_to_review');
412
+
413
+ CREATE VIEW v_active_pipeline AS
414
+ SELECT
415
+ j.id AS job_id,
416
+ c.name AS company_name,
417
+ j.title,
418
+ j.status,
419
+ j.score_total,
420
+ j.applied_at,
421
+ a.last_status_change_at
422
+ FROM jobs j
423
+ LEFT JOIN companies c ON c.id = j.company_id
424
+ LEFT JOIN applications a ON a.job_id = j.id
425
+ WHERE j.status IN ('applied','screen','onsite','offer')
426
+ ORDER BY a.last_status_change_at DESC NULLS LAST, j.applied_at DESC;
427
+
428
+ CREATE VIEW v_company_h1b_signal AS
429
+ SELECT
430
+ c.id AS company_id,
431
+ c.name AS company_name,
432
+ COUNT(h.id) AS total_filings,
433
+ SUM(CASE WHEN h.case_status = 'Certified' THEN 1 ELSE 0 END) AS certified_count,
434
+ MAX(h.decision_date) AS last_decision_date,
435
+ MAX(h.fiscal_year) AS most_recent_fy
436
+ FROM companies c
437
+ LEFT JOIN h1b_filings h ON h.employer_id = c.id
438
+ GROUP BY c.id, c.name;
439
+
440
+ -- Warm-intro pairing — job × non-recruiter connection at the same company.
441
+ -- contact_priority: 1=engineering peer, 2=leadership, 3=other; recruiters routed
442
+ -- separately so they're not in this view.
443
+ CREATE VIEW v_jobs_with_warm_intros AS
444
+ SELECT
445
+ j.id AS job_id,
446
+ c.id AS company_id,
447
+ c.name AS company_name,
448
+ j.title AS job_title,
449
+ j.score_total,
450
+ lc.id AS connection_id,
451
+ lc.full_name AS connection_name,
452
+ lc.position AS connection_position,
453
+ CASE
454
+ WHEN lc.is_engineering = 1 THEN 1
455
+ WHEN lc.is_leadership = 1 THEN 2
456
+ ELSE 3
457
+ END AS contact_priority
458
+ FROM jobs j
459
+ JOIN companies c ON c.id = j.company_id
460
+ JOIN linkedin_connections lc ON lc.company_id = c.id
461
+ WHERE lc.is_recruiter = 0
462
+ ORDER BY j.score_total DESC, contact_priority ASC;
463
+
464
+ -- Founder network — derives founder_kind via simple position substring match.
465
+ -- is_stealth: 1 when company_raw mentions stealth (we don't drop, we flag).
466
+ CREATE VIEW v_founder_network AS
467
+ SELECT
468
+ lc.id AS connection_id,
469
+ lc.full_name,
470
+ lc.position,
471
+ lc.company_raw,
472
+ lc.company_id,
473
+ CASE
474
+ WHEN LOWER(lc.position) LIKE '%founder%' THEN 'founder'
475
+ WHEN LOWER(lc.position) LIKE '%ceo%' THEN 'ceo'
476
+ WHEN LOWER(lc.position) LIKE '%cto%' THEN 'cto'
477
+ WHEN LOWER(lc.position) LIKE '%chief%' THEN 'c_suite'
478
+ ELSE 'other'
479
+ END AS founder_kind,
480
+ CASE
481
+ WHEN LOWER(COALESCE(lc.company_raw,'')) LIKE '%stealth%' THEN 1 ELSE 0
482
+ END AS is_stealth
483
+ FROM linkedin_connections lc
484
+ WHERE lc.is_leadership = 1
485
+ OR LOWER(lc.position) LIKE '%founder%'
486
+ OR LOWER(lc.position) LIKE '%ceo%'
487
+ OR LOWER(lc.position) LIKE '%cto%'
488
+ OR LOWER(lc.position) LIKE '%chief%';
489
+
490
+ CREATE VIEW v_followups_due AS
491
+ SELECT
492
+ o.id AS outreach_id,
493
+ o.connection_id,
494
+ lc.full_name AS connection_name,
495
+ c.name AS company_name,
496
+ o.outreach_type,
497
+ o.status,
498
+ o.sent_at,
499
+ o.followup_due_at
500
+ FROM outreach o
501
+ LEFT JOIN linkedin_connections lc ON lc.id = o.connection_id
502
+ LEFT JOIN companies c ON c.id = o.company_id
503
+ WHERE o.status = 'sent'
504
+ AND o.followup_due_at IS NOT NULL
505
+ AND o.followup_due_at <= CURRENT_TIMESTAMP;
@@ -0,0 +1,42 @@
1
+ -- M2 additions:
2
+ -- - llm_calls: per-call telemetry for cost_estimate() + parse-error visibility
3
+ -- - digest_state: tracks the cutoff timestamp of the last daily_digest run
4
+ -- - scan_runs: per scan_portals invocation summary (added in G2)
5
+
6
+ CREATE TABLE llm_calls (
7
+ id TEXT PRIMARY KEY,
8
+ tool TEXT NOT NULL,
9
+ provider TEXT NOT NULL,
10
+ model TEXT NOT NULL,
11
+ job_id TEXT REFERENCES jobs(id) ON DELETE SET NULL,
12
+ input_chars INTEGER NOT NULL,
13
+ output_chars INTEGER NOT NULL,
14
+ parse_ok INTEGER NOT NULL DEFAULT 1,
15
+ parse_error TEXT,
16
+ duration_ms INTEGER NOT NULL,
17
+ created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP
18
+ );
19
+
20
+ -- The only reader (cost_estimate) is a time-window scan grouped by (provider, model, tool).
21
+ -- A single descending btree on created_at supports that scan; per-column ones add write
22
+ -- cost without speeding up the GROUP BY.
23
+ CREATE INDEX idx_llm_calls_created_at ON llm_calls(created_at DESC);
24
+
25
+ CREATE TABLE digest_state (
26
+ id INTEGER PRIMARY KEY CHECK (id = 1),
27
+ last_digest_at TEXT
28
+ );
29
+ INSERT INTO digest_state (id) VALUES (1);
30
+
31
+ CREATE TABLE scan_runs (
32
+ id TEXT PRIMARY KEY,
33
+ started_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP,
34
+ finished_at TEXT,
35
+ sources TEXT, -- JSON array of provider ids hit
36
+ companies_n INTEGER NOT NULL DEFAULT 0,
37
+ jobs_found INTEGER NOT NULL DEFAULT 0,
38
+ jobs_new INTEGER NOT NULL DEFAULT 0,
39
+ jobs_dupes INTEGER NOT NULL DEFAULT 0,
40
+ errors_json TEXT, -- JSON array of { company, error }
41
+ triggered_by TEXT NOT NULL DEFAULT 'manual'
42
+ );
package/dist/server.js ADDED
@@ -0,0 +1,55 @@
1
+ // Entrypoint. One process: SQLite migrations, Express + file server, MCP HTTP transport.
2
+ import { buildHttpApp } from './http/app.js';
3
+ import { mountMcp } from './mcp/server.js';
4
+ import { config } from './config.js';
5
+ import { getDb } from './db.js';
6
+ import { ensureActiveCareerPacket, loadProjectFiles } from './core/profile.js';
7
+ import { closeBrowser } from './core/render.js';
8
+ import { shutdownScanResources } from './core/scan_engine.js';
9
+ import { applyState as applySchedulerState, readEnabledJobs } from './core/scheduler.js';
10
+ async function main() {
11
+ // Trigger migrations (side effect of getDb()).
12
+ getDb();
13
+ const seed = ensureActiveCareerPacket();
14
+ const files = loadProjectFiles();
15
+ const app = buildHttpApp();
16
+ mountMcp(app, '/mcp');
17
+ applySchedulerState();
18
+ const enabled = readEnabledJobs();
19
+ const server = app.listen(config.port, config.host, () => {
20
+ const baseUrl = config.baseUrl;
21
+ // eslint-disable-next-line no-console
22
+ console.error([
23
+ '',
24
+ `▷ mcp-jsa listening on ${baseUrl}`,
25
+ ` · MCP endpoint: ${baseUrl}/mcp`,
26
+ ` · Tracker UI: ${baseUrl}/`,
27
+ ` · File server: ${baseUrl}/files/*`,
28
+ ` · DB: ${config.dbPath}`,
29
+ ` · Project root: ${config.projectRoot}`,
30
+ ` · cv.md present: ${!!files.cvMd}`,
31
+ ` · profile.yml: ${!!files.profile}`,
32
+ ` · portals.yml: ${!!files.portalsYml}`,
33
+ ` · career_packet: ${seed.created ? `seeded v${seed.version}` : `existing v${seed.version}`}`,
34
+ ` · scheduler: ${enabled.length ? enabled.join(', ') : 'off'}`,
35
+ ` · visa scoring: ${config.visaScoringEnabled ? 'on (0.5/0.3/0.2)' : 'off (0.6/0.4, visa tools hidden)'}`,
36
+ '',
37
+ ].join('\n'));
38
+ });
39
+ const shutdown = async (signal) => {
40
+ // eslint-disable-next-line no-console
41
+ console.error(`\n[shutdown] ${signal} received`);
42
+ server.close();
43
+ await closeBrowser();
44
+ await shutdownScanResources();
45
+ process.exit(0);
46
+ };
47
+ process.on('SIGINT', () => { void shutdown('SIGINT'); });
48
+ process.on('SIGTERM', () => { void shutdown('SIGTERM'); });
49
+ }
50
+ main().catch((err) => {
51
+ // eslint-disable-next-line no-console
52
+ console.error('[fatal]', err);
53
+ process.exit(1);
54
+ });
55
+ //# sourceMappingURL=server.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.js","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"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"}
Binary file
Binary file
Binary file