@timurtekb/tekjobs 0.44.0 → 0.46.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 (37) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/app/dist/assets/{index-CIqEcxvl.css → index-Dhv_7hBU.css} +1 -1
  3. package/app/dist/assets/{index-D9GAewm_.js → index-V6WTCNu_.js} +10 -10
  4. package/app/dist/index.html +2 -2
  5. package/app/server/index.mjs +1 -1
  6. package/app/server/linkedin-import.mjs +99 -2
  7. package/app/server/mcp.mjs +6 -2
  8. package/cli.mjs +13 -1
  9. package/package.json +1 -1
  10. package/samples/vault/Jobs/Basalt Systems - Senior Design Engineer, Growth (1010).md +8 -8
  11. package/samples/vault/Jobs/Brightline Studio - Design Engineer (1005).md +3 -3
  12. package/samples/vault/Jobs/Copperleaf - Principal Design Engineer (1006).md +4 -4
  13. package/samples/vault/Jobs/Example Studio - Senior Design Engineer (1013).md +3 -3
  14. package/samples/vault/Jobs/Fjord Analytics - Staff Frontend Engineer, Platform (1003).md +8 -8
  15. package/samples/vault/Jobs/Halcyon Robotics - Senior UX Engineer (1004).md +3 -3
  16. package/samples/vault/Jobs/Lumen Health - Design Systems Engineer (1002).md +3 -3
  17. package/samples/vault/Jobs/Meridian Pay - Staff Design Engineer (1009).md +12 -12
  18. package/samples/vault/Jobs/Northwind Labs - Staff Design Engineer, Design Systems (1000).md +5 -5
  19. package/samples/vault/Jobs/Orbital Software - Senior Design Engineer (1001).md +3 -3
  20. package/samples/vault/Jobs/Quill & Co - Design Engineer, Editor (1008).md +5 -5
  21. package/samples/vault/Jobs/Signalfire Design - Design Engineer (1012).md +4 -4
  22. package/samples/vault/Jobs/Tessellate - Senior Frontend Engineer, Design Systems (1007).md +3 -3
  23. package/samples/vault/Jobs/Verdant - UX Engineer (1011).md +4 -4
  24. package/samples/vault/Logs/{2026-09-16.md → 2026-10-01.md} +3 -3
  25. package/samples/vault/Logs/{2026-09-23.md → 2026-10-08.md} +3 -3
  26. package/samples/vault/People/Lee Example (Fjord Analytics).md +3 -3
  27. package/samples/vault/People/Priya Example (Meridian Pay).md +4 -4
  28. package/samples/vault/People/Sam Example (Northwind Labs).md +1 -1
  29. package/samples/vault/Profile/Positioning.md +1 -1
  30. package/samples/vault/Profile/Profile.md +1 -1
  31. package/samples/vault/Profile/Resume.md +1 -1
  32. package/samples/vault/Profile/Voice.md +1 -1
  33. package/samples/vault/README.md +1 -1
  34. package/samples/vault/Targets/Companies.md +4 -4
  35. package/samples/vault/Targets/Search Criteria.before-2026-09-18.md +82 -0
  36. package/samples/vault/Targets/Search Criteria.md +8 -2
  37. package/samples/vault/_Home.md +12 -13
@@ -8,8 +8,8 @@
8
8
  <link rel="icon" href="/brand/tekjobs-avatar-192.png" type="image/png" sizes="192x192" />
9
9
  <link rel="apple-touch-icon" href="/brand/tekjobs-avatar-192.png" />
10
10
  <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Bricolage+Grotesque:wght@500;600&family=Hanken+Grotesk:wght@400;500;600&family=Azeret+Mono:wght@400;500&display=swap" data-zengin="fonts" />
11
- <script type="module" crossorigin src="/assets/index-D9GAewm_.js"></script>
12
- <link rel="stylesheet" crossorigin href="/assets/index-CIqEcxvl.css">
11
+ <script type="module" crossorigin src="/assets/index-V6WTCNu_.js"></script>
12
+ <link rel="stylesheet" crossorigin href="/assets/index-Dhv_7hBU.css">
13
13
  </head>
14
14
  <body>
15
15
  <div id="root"></div>
@@ -41,7 +41,7 @@ const routes = [
41
41
  ['GET', /^\/api\/jobs\/([^/]+)\/connections$/, (m) => linkedin.connectionsAt(store.getJob(decodeURIComponent(m[1])).company)],
42
42
  ['GET', /^\/api\/linkedin$/, () => linkedin.status()],
43
43
  ['POST', /^\/api\/linkedin\/preview$/, async (_, __, req) => { const b = await readBody(req); return linkedin.preview(b.source, { since: b.since || undefined, everyone: !!b.everyone }); }],
44
- ['POST', /^\/api\/linkedin\/import$/, async (_, __, req) => { const b = await readBody(req); return linkedin.runImport(b.source, { since: b.since || undefined, everyone: !!b.everyone, writePeople: b.writePeople !== false, dry: !!b.dry }); }],
44
+ ['POST', /^\/api\/linkedin\/import$/, async (_, __, req) => { const b = await readBody(req); const r = linkedin.runImport(b.source, { since: b.since || undefined, everyone: !!b.everyone, writePeople: b.writePeople !== false, dry: !!b.dry }); if (b.applications || b.saved) r.jobs = linkedin.importJobs(b.source, { since: b.since || undefined, applications: !!b.applications, saved: !!b.saved, dry: !!b.dry }); return r; }],
45
45
  ['GET', /^\/api\/people$/, () => people.listPeople()],
46
46
  ['POST', /^\/api\/people$/, async (_, __, req) => { const b = await readBody(req); const p = people.createPerson(b); if (b.jobId) people.attachPerson(b.jobId, p.id, { role: b.role, context: b.context }); return people.getPerson(p.id); }],
47
47
  ['GET', /^\/api\/people\/([^/]+)$/, (m) => people.getPerson(decodeURIComponent(m[1]))],
@@ -4,10 +4,13 @@
4
4
  // appended again.
5
5
  import fs from 'node:fs';
6
6
  import path from 'node:path';
7
- import { P, DATA_DIR } from '../../scraper/config.mjs';
8
- import { readExport, buildIndex, peopleCandidates, warmPaths, companyKey } from '../../scraper/linkedin.mjs';
7
+ import { loadCriteria, P, DATA_DIR } from '../../scraper/config.mjs';
8
+ import { readExport, buildIndex, peopleCandidates, warmPaths, companyKey, sameCompany } from '../../scraper/linkedin.mjs';
9
9
  import * as people from './people.mjs';
10
10
  import * as store from './store.mjs';
11
+ import { writeJobNote } from '../../scraper/vault.mjs';
12
+ import { scoreJob, normalizeTitle } from '../../scraper/score.mjs';
13
+ import { htmlToText } from '../../scraper/sources.mjs';
11
14
 
12
15
  const isoDay = (d = new Date()) => d.toISOString().slice(0, 10);
13
16
  const daysAgo = (n) => isoDay(new Date(Date.now() - n * 864e5));
@@ -107,3 +110,97 @@ export function runImport(source, { since = daysAgo(90), roles = ['recruiter', '
107
110
 
108
111
  return summary;
109
112
  }
113
+
114
+ // ---------------- applications and saved jobs, as notes ----------------
115
+ // The export carries the person's own record of where they applied through LinkedIn and what they saved. Opt-in:
116
+ // each becomes a note (or a line on the note that already exists), so the pipeline shows the search they were
117
+ // already running. The posting itself is never fetched; the note carries the LinkedIn link for the person to open.
118
+ const titleKey = (t) => normalizeTitle(t).replace(/[^a-z0-9 ]+/g, ' ').replace(/\s+/g, ' ').trim();
119
+ const sameTitle = (a, b) => { const x = titleKey(a), y = titleKey(b); return !!x && !!y && (x === y || x.includes(y) || y.includes(x)); };
120
+ const hash = (s) => { let h = 5381; for (const ch of String(s)) h = ((h * 33) ^ ch.charCodeAt(0)) >>> 0; return h.toString(16); };
121
+ const noteText = (id) => { try { return fs.readFileSync(store.getJob(id).path, 'utf8'); } catch { return ''; } };
122
+ // Empty or absent: a note written by hand or by an older version may not carry the field at all.
123
+ const appliedOnEmpty = (id) => !/^- \*\*Applied on:\*\*\s*\S/m.test(noteText(id));
124
+ const noteOnce = (id, line) => { if (!noteText(id).includes(line)) store.addNote(id, line, 'linkedin-export'); };
125
+
126
+ function findNote(jobs, { company, title, url }) {
127
+ const clean = (u) => String(u || '').split('?')[0].replace(/\/$/, '');
128
+ const byUrl = url && jobs.find((j) => j.url && clean(j.url) === clean(url));
129
+ if (byUrl) return byUrl;
130
+ return jobs.filter((j) => sameCompany(j.company, company)).find((j) => sameTitle(j.title, title)) || null;
131
+ }
132
+ function stubFor(kind, r) {
133
+ const html = kind === 'application'
134
+ ? `<p>Applied through LinkedIn on ${r.date}. From the LinkedIn data export: the posting itself was not read, so there is no description or pay here. Open the link for the posting.</p>`
135
+ : `<p>Saved on LinkedIn on ${r.date}. From the LinkedIn data export: the posting itself was not read. Open the link for the posting.</p>`;
136
+ const job = {
137
+ id: `linkedin:${kind}:${hash(`${r.url || ''}|${r.company}|${r.title}|${r.date}`)}`, source: 'linkedin-export', company: r.company, title: r.title || 'Role not named in the export',
138
+ url: r.url || '', location: '', remote: false, posted: r.date, descriptionHtml: html, foundVia: kind === 'application' ? 'from the LinkedIn export: an application' : 'from the LinkedIn export: a saved job', addedBy: 'linkedin-export',
139
+ };
140
+ job.descriptionText = htmlToText(job.descriptionHtml);
141
+ return job;
142
+ }
143
+
144
+ /** What importing the applications and saved jobs would touch: per kind, the notes it would mark and the ones it would create. */
145
+ export function planJobs(index, { since = index?.since || '', applications = false, saved = false } = {}) {
146
+ const jobs = store.listJobs();
147
+ const recent = (d) => !since || (d && d >= since);
148
+ const one = (rows, on) => {
149
+ const out = { matched: [], create: [], skipped: 0 };
150
+ if (!on) return out;
151
+ for (const r of rows || []) {
152
+ if (!recent(r.date) || !r.company) { out.skipped++; continue; }
153
+ const note = findNote(jobs, r);
154
+ if (note) out.matched.push({ id: note.id, status: note.status, date: r.date, company: r.company, title: r.title });
155
+ else out.create.push(r);
156
+ }
157
+ return out;
158
+ };
159
+ return { since, applications: one(index?.applications, applications), saved: one(index?.savedJobs, saved) };
160
+ }
161
+
162
+ /**
163
+ * Applications become notes at "applied" with the date; saved jobs become notes at "reviewing". A note that already
164
+ * exists gets the line and, for an application, the status and the date if it had none. Re-running changes nothing.
165
+ * The status is set via "linkedin-export": the person's own record is the evidence, so the human-only rule for agents
166
+ * does not apply, and the status log says where it came from.
167
+ */
168
+ export function importJobs(source, { since = daysAgo(90), applications = false, saved = false, dry = false } = {}) {
169
+ const index = buildIndex(readExport(source), { since });
170
+ const plan = planJobs(index, { since: index.since, applications, saved });
171
+ const out = { since: index.since, dry, applications: { matched: 0, marked: 0, created: 0, skipped: plan.applications.skipped }, saved: { matched: 0, created: 0, skipped: plan.saved.skipped } };
172
+ // A profile with no criteria yet (the import can run before the interview) still gets its notes; they are scored later by a rescore.
173
+ let criteria; try { criteria = loadCriteria(); } catch { criteria = { titleTerms: {}, titleExclude: [], descTerms: {}, minScore: 0 }; }
174
+ const advanced = ['applied', 'interviewing', 'offer', 'rejected'];
175
+ for (const m of plan.applications.matched) {
176
+ out.applications.matched++;
177
+ if (dry) continue;
178
+ if (!advanced.includes(m.status)) { store.setStatus(m.id, 'applied', 'linkedin-export'); out.applications.marked++; }
179
+ if (appliedOnEmpty(m.id)) store.saveApplicationDraft(m.id, { field: 'Applied on', value: m.date });
180
+ noteOnce(m.id, `Applied through LinkedIn on ${m.date} (from the LinkedIn export).`);
181
+ }
182
+ for (const r of plan.applications.create) {
183
+ out.applications.created++;
184
+ if (dry) continue;
185
+ const job = stubFor('application', r);
186
+ const id = path.basename(writeJobNote(job, scoreJob(job, criteria), criteria), '.md');
187
+ store.cacheClear?.();
188
+ store.setStatus(id, 'applied', 'linkedin-export');
189
+ if (appliedOnEmpty(id)) store.saveApplicationDraft(id, { field: 'Applied on', value: r.date });
190
+ }
191
+ for (const m of plan.saved.matched) {
192
+ out.saved.matched++;
193
+ if (dry) continue;
194
+ noteOnce(m.id, `Saved on LinkedIn on ${m.date} (from the LinkedIn export).`);
195
+ }
196
+ for (const r of plan.saved.create) {
197
+ out.saved.created++;
198
+ if (dry) continue;
199
+ const job = stubFor('saved', r);
200
+ const id = path.basename(writeJobNote(job, scoreJob(job, criteria), criteria), '.md');
201
+ store.cacheClear?.();
202
+ store.setStatus(id, 'reviewing', 'linkedin-export');
203
+ }
204
+ store.cacheClear?.();
205
+ return out;
206
+ }
@@ -19,7 +19,7 @@ export const TOOLS = [
19
19
  { name: 'list_snippets', description: 'The copy panel: the person\'s standard answers for application forms (name, email, phone, links, availability, salary answer, anything they added), grouped, from Profile/Snippets.md. Use these verbatim when drafting form answers; never invent a value that is empty here.', inputSchema: { type: 'object', properties: {} } },
20
20
  { name: 'list_people', description: 'The people in the search: recruiters, hiring managers, interviewers and referrals, one note each under People/, with role, company, email, last contact and the job notes they are on. Newest contact first.', inputSchema: { type: 'object', properties: {} } },
21
21
  { name: 'get_person', description: 'One person in full: the row plus their About text and dated Log of contacts.', inputSchema: { type: 'object', properties: { id: { type: 'string' } }, required: ['id'] } },
22
- { name: 'import_linkedin', description: "Read the person's LinkedIn data export (the larger archive from linkedin.com/mypreferences/d/download-my-data, as a zip or an unpacked folder, given by local path) into the search: an index of who they know at which company (job notes then show connections), People notes for the recruiters and hiring managers who wrote since the date (with the thread as their log, linked to open job notes at that company). The archive is read in place and never copied. dry=true reports without writing; everyone=true also writes senders whose title is not a recruiting or hiring one. Default since: 90 days ago.", inputSchema: { type: 'object', properties: { source: { type: 'string', description: 'Local path to the zip or folder' }, since: { type: 'string', description: 'YYYY-MM-DD' }, everyone: { type: 'boolean' }, writePeople: { type: 'boolean' }, dry: { type: 'boolean' } }, required: ['source'] } },
22
+ { name: 'import_linkedin', description: "Read the person's LinkedIn data export (the larger archive from linkedin.com/mypreferences/d/download-my-data, as a zip or an unpacked folder, given by local path) into the search: an index of who they know at which company (job notes then show connections), People notes for the recruiters and hiring managers who wrote since the date (with the thread as their log, linked to open job notes at that company). The archive is read in place and never copied. dry=true reports without writing; everyone=true also writes senders whose title is not a recruiting or hiring one. applications=true turns the person's LinkedIn applications since the date into notes at applied with the date (or marks the note that exists); saved=true turns their saved jobs into notes at reviewing. The person's own export is the evidence, so these statuses are allowed here. Default since: 90 days ago.", inputSchema: { type: 'object', properties: { source: { type: 'string', description: 'Local path to the zip or folder' }, since: { type: 'string', description: 'YYYY-MM-DD' }, everyone: { type: 'boolean' }, writePeople: { type: 'boolean' }, applications: { type: 'boolean' }, saved: { type: 'boolean' }, dry: { type: 'boolean' } }, required: ['source'] } },
23
23
  { name: 'connections_at', description: "Who the person knows at a company, from the LinkedIn import: name, title, a guessed role (recruiter, hiring-manager, other), profile link, when connected. Empty before the first import (say so and offer import_linkedin). Use it before drafting an application or when a job note is discussed: a warm introduction beats a cold apply.", inputSchema: { type: 'object', properties: { company: { type: 'string' } }, required: ['company'] } },
24
24
  { name: 'add_person', description: 'Add a person (or recognise one already there, by email or name and company) and optionally put them on a job note. role: recruiter | hiring-manager | interviewer | referral | other. Give jobId to attach; context is a few words on the thread.', inputSchema: { type: 'object', properties: { name: { type: 'string' }, role: { type: 'string', enum: people.ROLES }, company: { type: 'string' }, email: { type: 'string' }, links: { type: 'string' }, about: { type: 'string' }, jobId: { type: 'string' }, context: { type: 'string' } }, required: ['name'] } },
25
25
  { name: 'attach_person', description: 'Put an existing person on a job note (a line under its People section) and the job on their Threads. Idempotent.', inputSchema: { type: 'object', properties: { jobId: { type: 'string' }, personId: { type: 'string' }, role: { type: 'string', enum: people.ROLES }, context: { type: 'string' } }, required: ['jobId', 'personId'] } },
@@ -116,7 +116,11 @@ export async function call(name, a = {}) {
116
116
  case 'list_snippets': return store.getSnippets();
117
117
  case 'list_people': return people.listPeople();
118
118
  case 'get_person': return people.getPerson(a.id);
119
- case 'import_linkedin': return linkedin.runImport(a.source, { since: a.since, everyone: !!a.everyone, writePeople: a.writePeople !== false, dry: !!a.dry });
119
+ case 'import_linkedin': {
120
+ const r = linkedin.runImport(a.source, { since: a.since, everyone: !!a.everyone, writePeople: a.writePeople !== false, dry: !!a.dry });
121
+ if (a.applications || a.saved) r.jobs = linkedin.importJobs(a.source, { since: a.since, applications: !!a.applications, saved: !!a.saved, dry: !!a.dry });
122
+ return r;
123
+ }
120
124
  case 'connections_at': return linkedin.connectionsAt(a.company);
121
125
  case 'add_person': { const p = people.createPerson(a); if (a.jobId) people.attachPerson(a.jobId, p.id, { role: a.role, context: a.context }); return people.getPerson(p.id); }
122
126
  case 'attach_person': return people.attachPerson(a.jobId, a.personId, { role: a.role, context: a.context });
package/cli.mjs CHANGED
@@ -40,6 +40,8 @@ const HELP = `tekjobs — a local job-search machine
40
40
  tekjobs import linkedin <zip|folder> your LinkedIn data export into the search: who you know at each
41
41
  company, and the recruiters who wrote
42
42
  [--since YYYY-MM-DD] [--everyone] [--no-people] [--preview] [--dry]
43
+ [--applications] your LinkedIn applications as notes at "applied", with the date
44
+ [--saved] your saved jobs as notes at "reviewing"
43
45
  tekjobs up [--dev] [--port N] start the API (and with --dev, Vite) detached: they stay up until \`down\`
44
46
  tekjobs down stop what \`up\` started
45
47
  tekjobs ps what \`up\` has running
@@ -182,11 +184,21 @@ async function main() {
182
184
  if (!source) return console.error('Usage: tekjobs import linkedin <path to the export zip or unpacked folder> [--since YYYY-MM-DD] [--everyone] [--no-people] [--preview] [--dry]\nRequest the larger archive at https://www.linkedin.com/mypreferences/d/download-my-data');
183
185
  const li = await import('./app/server/linkedin-import.mjs');
184
186
  const o = { since: opt('--since') || undefined, everyone: rest.includes('--everyone'), writePeople: !rest.includes('--no-people'), dry: rest.includes('--dry') };
185
- if (rest.includes('--preview')) return console.log(JSON.stringify(li.preview(source, o), null, 2));
187
+ const jobsOpt = { since: o.since, applications: rest.includes('--applications'), saved: rest.includes('--saved'), dry: o.dry };
188
+ if (rest.includes('--preview')) {
189
+ const p = li.preview(source, o);
190
+ if (jobsOpt.applications || jobsOpt.saved) p.jobs = li.importJobs(source, { ...jobsOpt, dry: true });
191
+ return console.log(JSON.stringify(p, null, 2));
192
+ }
186
193
  const s = li.runImport(source, o);
194
+ const jobs = jobsOpt.applications || jobsOpt.saved ? li.importJobs(source, jobsOpt) : null;
187
195
  console.log(`LinkedIn export read from ${s.source}${s.dry ? ' (dry run, nothing written)' : ''}`);
188
196
  console.log(` ${s.counts.connections} connections indexed, ${s.counts.threads} conversations and ${s.counts.invitations} invitations since ${s.since}, ${s.counts.applications} applications, ${s.counts.savedJobs} saved jobs`);
189
197
  console.log(` People: ${s.people.created} created, ${s.people.recognised} already there, ${s.people.attached} put on job notes, ${s.people.logged} log lines; ${s.people.skipped} senders skipped (not recruiters or hiring managers; --everyone includes them)`);
198
+ if (jobs) {
199
+ if (jobsOpt.applications) console.log(` Applications: ${jobs.applications.matched} already had a note (${jobs.applications.marked} moved to applied), ${jobs.applications.created} notes created at applied, ${jobs.applications.skipped} before ${jobs.since} or without a company`);
200
+ if (jobsOpt.saved) console.log(` Saved jobs: ${jobs.saved.matched} already had a note, ${jobs.saved.created} notes created at reviewing, ${jobs.saved.skipped} before ${jobs.since} or without a company`);
201
+ }
190
202
  if (!s.dry) console.log(` Index: ${s.indexPath}. Job notes now show who you know at each company.`);
191
203
  return;
192
204
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@timurtekb/tekjobs",
3
- "version": "0.44.0",
3
+ "version": "0.46.0",
4
4
  "description": "A local-first job-search machine: watches hundreds of company job boards, scores every posting against your profile, and files matches into a folder of markdown notes you own. Bring your own LLM through MCP.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -10,8 +10,8 @@ source: ashby
10
10
  url: "https://jobs.example.com/basalt/senior-design-engineer-growth"
11
11
  score: 114
12
12
  weights: 3e895160
13
- posted: "2026-08-29"
14
- found: 2026-08-30
13
+ posted: "2026-09-13"
14
+ found: 2026-09-14
15
15
  salary: "$180k–$220k"
16
16
  salary_max: 220000
17
17
  pay_band: floor
@@ -33,14 +33,14 @@ tags: [job]
33
33
  - posted 1d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-08-30 — found by scraper (score 114). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
- - 2026-09-01 — new → **reviewing** (via app)
38
- - 2026-09-03 — reviewing → **applying** (via app)
39
- - 2026-09-05 — applying → **applied** (via app)
40
- - 2026-09-17 — applied → **rejected** (via app)
36
+ - 2026-09-14 — found by scraper (score 114). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
+ - 2026-09-16 — new → **reviewing** (via app)
38
+ - 2026-09-18 — reviewing → **applying** (via app)
39
+ - 2026-09-20 — applying → **applied** (via app)
40
+ - 2026-10-02 — applied → **rejected** (via app)
41
41
 
42
42
  ## Notes
43
- - 2026-09-17 (app): Form rejection after twelve days. No reason given.
43
+ - 2026-10-02 (app): Form rejection after twelve days. No reason given.
44
44
 
45
45
  ## Application
46
46
  - **Narrative:**
@@ -10,8 +10,8 @@ source: lever
10
10
  url: "https://jobs.example.com/brightline/design-engineer"
11
11
  score: 95
12
12
  weights: 3e895160
13
- posted: "2026-09-22"
14
- found: 2026-09-22
13
+ posted: "2026-10-07"
14
+ found: 2026-10-07
15
15
  salary: "$160k–$195k"
16
16
  salary_max: 195000
17
17
  pay_band: stretch
@@ -32,7 +32,7 @@ tags: [job, stretch]
32
32
  - posted 0d ago (+25)
33
33
 
34
34
  ## Status log
35
- - 2026-09-22 — found by scraper (score 95). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
35
+ - 2026-10-07 — found by scraper (score 95). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
36
36
 
37
37
  ## Notes
38
38
 
@@ -10,8 +10,8 @@ source: greenhouse
10
10
  url: "https://jobs.example.com/copperleaf/principal-design-engineer"
11
11
  score: 134
12
12
  weights: 3e895160
13
- posted: "2026-09-19"
14
- found: 2026-09-20
13
+ posted: "2026-10-04"
14
+ found: 2026-10-05
15
15
  salary: "$240k–$290k"
16
16
  salary_max: 290000
17
17
  pay_band: floor
@@ -33,8 +33,8 @@ tags: [job]
33
33
  - posted 1d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-09-20 — found by scraper (score 134). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
- - 2026-09-20 — new → **reviewing** (via app)
36
+ - 2026-10-05 — found by scraper (score 134). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
+ - 2026-10-05 — new → **reviewing** (via app)
38
38
 
39
39
  ## Notes
40
40
 
@@ -10,8 +10,8 @@ source: tekjobs
10
10
  url: "https://tekjobs.timurtek.com/jobs/example-tekjobs"
11
11
  score: 130
12
12
  weights: 3e895160
13
- posted: "2026-09-23"
14
- found: 2026-09-23
13
+ posted: "2026-10-08"
14
+ found: 2026-10-08
15
15
  salary: "$180k–$240k"
16
16
  salary_max: 240000
17
17
  pay_band: floor
@@ -33,7 +33,7 @@ tags: [job]
33
33
  - posted 0d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-09-23 — found by scraper (score 130). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
36
+ - 2026-10-08 — found by scraper (score 130). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
37
 
38
38
  ## Notes
39
39
 
@@ -10,8 +10,8 @@ source: greenhouse
10
10
  url: "https://jobs.example.com/fjord/staff-frontend-engineer-platform"
11
11
  score: 111
12
12
  weights: 3e895160
13
- posted: "2026-09-14"
14
- found: 2026-09-15
13
+ posted: "2026-09-29"
14
+ found: 2026-09-30
15
15
  salary: "$220k–$270k"
16
16
  salary_max: 270000
17
17
  pay_band: floor
@@ -33,10 +33,10 @@ tags: [job]
33
33
  - posted 1d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-09-15 — found by scraper (score 111). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
- - 2026-09-16 — new → **reviewing** (via app)
38
- - 2026-09-17 — reviewing → **applying** (via app)
39
- - 2026-09-19 — applying → **applied** (via app)
36
+ - 2026-09-30 — found by scraper (score 111). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
+ - 2026-10-01 — new → **reviewing** (via app)
38
+ - 2026-10-02 — reviewing → **applying** (via app)
39
+ - 2026-10-04 — applying → **applied** (via app)
40
40
 
41
41
  ## Notes
42
42
 
@@ -45,9 +45,9 @@ tags: [job]
45
45
  - **Narrative:**
46
46
  - **Resume variant:**
47
47
  - **Cover letter:**
48
- - **Applied on:** 2026-09-19
48
+ - **Applied on:** 2026-10-04
49
49
  - **Contact / referral:**
50
- - **Follow-up due:** 2026-09-26
50
+ - **Follow-up due:** 2026-10-11
51
51
 
52
52
  ## People
53
53
  - [[People/Lee Example (Fjord Analytics)|Lee Example]] · referral · lee@example.com · submitted the internal referral
@@ -10,8 +10,8 @@ source: ashby
10
10
  url: "https://jobs.example.com/halcyon/senior-ux-engineer"
11
11
  score: 106
12
12
  weights: 3e895160
13
- posted: "2026-09-08"
14
- found: 2026-09-09
13
+ posted: "2026-09-23"
14
+ found: 2026-09-24
15
15
  salary: "$180k–$215k"
16
16
  salary_max: 215000
17
17
  pay_band: floor
@@ -33,7 +33,7 @@ tags: [job]
33
33
  - posted 1d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-09-09 — found by scraper (score 106). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
36
+ - 2026-09-24 — found by scraper (score 106). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
37
 
38
38
  ## Notes
39
39
 
@@ -10,8 +10,8 @@ source: lever
10
10
  url: "https://jobs.example.com/lumen/design-systems-engineer"
11
11
  score: 109
12
12
  weights: 3e895160
13
- posted: "2026-09-11"
14
- found: 2026-09-12
13
+ posted: "2026-09-26"
14
+ found: 2026-09-27
15
15
  salary: "$175k–$205k"
16
16
  salary_max: 205000
17
17
  pay_band: floor
@@ -32,7 +32,7 @@ tags: [job]
32
32
  - posted 1d ago (+25)
33
33
 
34
34
  ## Status log
35
- - 2026-09-12 — found by scraper (score 109). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
35
+ - 2026-09-27 — found by scraper (score 109). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
36
36
 
37
37
  ## Notes
38
38
 
@@ -10,8 +10,8 @@ source: greenhouse
10
10
  url: "https://jobs.example.com/meridian/staff-design-engineer"
11
11
  score: 137
12
12
  weights: 3e895160
13
- posted: "2026-09-02"
14
- found: 2026-09-03
13
+ posted: "2026-09-17"
14
+ found: 2026-09-18
15
15
  salary: "$225k–$275k"
16
16
  salary_max: 275000
17
17
  pay_band: floor
@@ -33,28 +33,28 @@ tags: [job]
33
33
  - posted 1d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-09-03 — found by scraper (score 137). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
- - 2026-09-04 — new → **reviewing** (via app)
38
- - 2026-09-06 — reviewing → **applying** (via app)
39
- - 2026-09-11 — applying → **applied** (via app)
40
- - 2026-09-18 — applied → **interviewing** (via app)
36
+ - 2026-09-18 — found by scraper (score 137). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
+ - 2026-09-19 — new → **reviewing** (via app)
38
+ - 2026-09-21 — reviewing → **applying** (via app)
39
+ - 2026-09-26 — applying → **applied** (via app)
40
+ - 2026-10-03 — applied → **interviewing** (via app)
41
41
 
42
42
  ## Notes
43
- - 2026-09-18 (app): Recruiter screen with Priya went well; technical interview with the platform lead scheduled for next week. She asked for the Storybook link.
43
+ - 2026-10-03 (app): Recruiter screen with Priya went well; technical interview with the platform lead scheduled for next week. She asked for the Storybook link.
44
44
 
45
45
  ## Application
46
46
  - **Narrative:** Design engineer; the Acme system numbers lead (62 components, 12 teams, defects down a third).
47
47
  - **Resume variant:** Design Engineer (Staff)
48
- - **Cover letter:** Drafted 2026-09-11, 107 words. Full text under "Cover letter" in this note.
49
- - **Applied on:** 2026-09-11
48
+ - **Cover letter:** Drafted 2026-09-26, 107 words. Full text under "Cover letter" in this note.
49
+ - **Applied on:** 2026-09-26
50
50
  - **Contact / referral:**
51
- - **Follow-up due:** 2026-09-18
51
+ - **Follow-up due:** 2026-10-03
52
52
  - **Tailored summary:** Design engineer with eight years on design systems; led the Acme Finance system from 4 to 62 components across 12 teams, including white-label theming for partner banks.
53
53
  - **Tailored bullets:** System architecture and tokens · white-label theming · the ESM migration (1.9 MB to 640 kB) · the WCAG 2.1 AA audit with zero blockers
54
54
  - **Risks:** No payroll domain experience; say so, and point at fintech at Acme.
55
55
 
56
56
  ## Cover letter
57
- > Drafted 2026-09-11 (via app), 107 words. A draft: read it, edit it, send it yourself.
57
+ > Drafted 2026-09-26 (via app), 107 words. A draft: read it, edit it, send it yourself.
58
58
 
59
59
  Hello. Your posting says Meridian Pay's design system serves six product teams and the white-label theming your partners depend on. At Acme Finance I took the system from 4 components to 62, used by 12 teams, with partner theming built on the same tokens; UI defect reports fell by a third in the year after. The part I would bring first is the token pipeline, because a system that themes for partners lives or dies on it. In the first month I would inventory the two legacy apps against the system and write down what they still need, since that list is the migration plan. Jordan Example.
60
60
 
@@ -10,8 +10,8 @@ source: greenhouse
10
10
  url: "https://jobs.example.com/northwind/staff-design-engineer-design-systems"
11
11
  score: 148
12
12
  weights: 3e895160
13
- posted: "2026-09-20"
14
- found: 2026-09-21
13
+ posted: "2026-10-05"
14
+ found: 2026-10-06
15
15
  salary: "$215k–$265k"
16
16
  salary_max: 265000
17
17
  pay_band: floor
@@ -33,9 +33,9 @@ tags: [job]
33
33
  - posted 1d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-09-21 — found by scraper (score 148). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
- - 2026-09-21 — new → **reviewing** (via app)
38
- - 2026-09-22 — reviewing → **applying** (via app)
36
+ - 2026-10-06 — found by scraper (score 148). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
+ - 2026-10-06 — new → **reviewing** (via app)
38
+ - 2026-10-07 — reviewing → **applying** (via app)
39
39
 
40
40
  ## Notes
41
41
 
@@ -10,8 +10,8 @@ source: ashby
10
10
  url: "https://jobs.example.com/orbital/senior-design-engineer"
11
11
  score: 129
12
12
  weights: 3e895160
13
- posted: "2026-09-17"
14
- found: 2026-09-18
13
+ posted: "2026-10-02"
14
+ found: 2026-10-03
15
15
  salary: "$190k–$230k"
16
16
  salary_max: 230000
17
17
  pay_band: floor
@@ -33,7 +33,7 @@ tags: [job]
33
33
  - posted 1d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-09-18 — found by scraper (score 129). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
36
+ - 2026-10-03 — found by scraper (score 129). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
37
 
38
38
  ## Notes
39
39
 
@@ -11,8 +11,8 @@ source: lever
11
11
  url: "https://jobs.example.com/quill/design-engineer-editor"
12
12
  score: 63
13
13
  weights: 3e895160
14
- posted: "2026-09-05"
15
- found: 2026-09-06
14
+ posted: "2026-09-20"
15
+ found: 2026-09-21
16
16
  salary: "$170k–$210k"
17
17
  salary_max: 210000
18
18
  pay_band: floor
@@ -34,9 +34,9 @@ tags: [job]
34
34
  - posted 1d ago (+25)
35
35
 
36
36
  ## Status log
37
- - 2026-09-06 — found by scraper (score 63). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
38
- - 2026-09-07 — new → **reviewing** (via app)
39
- - 2026-09-08 — reviewing → **passed** (wrong role shape) (via app)
37
+ - 2026-09-21 — found by scraper (score 63). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
38
+ - 2026-09-22 — new → **reviewing** (via app)
39
+ - 2026-09-23 — reviewing → **passed** (wrong role shape) (via app)
40
40
 
41
41
  ## Notes
42
42
 
@@ -11,8 +11,8 @@ source: lever
11
11
  url: "https://jobs.example.com/signalfire/design-engineer"
12
12
  score: 23
13
13
  weights: 3e895160
14
- posted: "2026-09-21"
15
- found: 2026-09-21
14
+ posted: "2026-10-06"
15
+ found: 2026-10-06
16
16
  salary: "$170k–$200k"
17
17
  salary_max: 200000
18
18
  pay_band: floor
@@ -33,8 +33,8 @@ tags: [job]
33
33
  - posted 0d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-09-21 — found by scraper (score 23). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
- - 2026-09-22 — new → **passed** (location or authorisation) (via app)
36
+ - 2026-10-06 — found by scraper (score 23). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
+ - 2026-10-07 — new → **passed** (location or authorisation) (via app)
38
38
 
39
39
  ## Notes
40
40
 
@@ -10,8 +10,8 @@ source: ashby
10
10
  url: "https://jobs.example.com/tessellate/senior-frontend-engineer-design-systems"
11
11
  score: 136
12
12
  weights: 3e895160
13
- posted: "2026-09-16"
14
- found: 2026-09-17
13
+ posted: "2026-10-01"
14
+ found: 2026-10-02
15
15
  salary: "$185k–$225k"
16
16
  salary_max: 225000
17
17
  pay_band: floor
@@ -33,7 +33,7 @@ tags: [job]
33
33
  - posted 1d ago (+25)
34
34
 
35
35
  ## Status log
36
- - 2026-09-17 — found by scraper (score 136). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
36
+ - 2026-10-02 — found by scraper (score 136). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
37
37
 
38
38
  ## Notes
39
39
 
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  type: job
3
3
  status: new
4
- listing: closed 2026-09-14
4
+ listing: closed 2026-09-29
5
5
  company: "Verdant"
6
6
  title: "UX Engineer"
7
7
  location: "Remote (United States)"
@@ -10,8 +10,8 @@ source: greenhouse
10
10
  url: "https://jobs.example.com/verdant/ux-engineer"
11
11
  score: 77
12
12
  weights: 3e895160
13
- posted: "2026-08-24"
14
- found: 2026-08-25
13
+ posted: "2026-09-08"
14
+ found: 2026-09-09
15
15
  salary: "$165k–$190k"
16
16
  salary_max: 190000
17
17
  pay_band: stretch
@@ -32,7 +32,7 @@ tags: [job, stretch]
32
32
  - posted 1d ago (+25)
33
33
 
34
34
  ## Status log
35
- - 2026-08-25 — found by scraper (score 77). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
35
+ - 2026-09-09 — found by scraper (score 77). Set `status:` above to `reviewing`, `applying`, `applied`, `interviewing`, `offer`, `rejected`, or `passed`.
36
36
 
37
37
  ## Notes
38
38
 
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  type: log
3
- date: 2026-09-16
3
+ date: 2026-10-01
4
4
  ---
5
- # 2026-09-16 — scraper log
5
+ # 2026-10-01 — scraper log
6
6
 
7
- ## Run 2026-09-16 07:30 UTC
7
+ ## Run 2026-10-01 07:30 UTC
8
8
  - Boards: 304/304 ok · postings scanned: 24812 · scored ≥ 45: 394 · **new: 9** · closed: 0 · 88.4s
9
9
  - Criteria: Search Criteria
10
10
  - TekJobs postings: 1 postings
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  type: log
3
- date: 2026-09-23
3
+ date: 2026-10-08
4
4
  ---
5
- # 2026-09-23 — scraper log
5
+ # 2026-10-08 — scraper log
6
6
 
7
- ## Run 2026-09-23 07:30 UTC
7
+ ## Run 2026-10-08 07:30 UTC
8
8
  - Boards: 304/304 ok · postings scanned: 25340 · scored ≥ 45: 394 · **new: 3** · closed: 1 · 88.4s
9
9
  - Criteria: Search Criteria
10
10
  - TekJobs postings: 1 postings
@@ -5,8 +5,8 @@ role: referral
5
5
  company: "Fjord Analytics"
6
6
  email: "lee@example.com"
7
7
  links: ""
8
- created: 2026-09-17
9
- last_contact: 2026-09-17
8
+ created: 2026-10-02
9
+ last_contact: 2026-10-02
10
10
  ---
11
11
 
12
12
  # Lee Example
@@ -18,5 +18,5 @@ Former Widgets Inc colleague, now on Fjord's data team.
18
18
  - [[Jobs/Fjord Analytics - Staff Frontend Engineer, Platform (1003)|Fjord Analytics - Staff Frontend Engineer, Platform]] · referral
19
19
 
20
20
  ## Log
21
- - 2026-09-17 (app): Asked for a referral; Lee submitted it the same afternoon.
21
+ - 2026-10-02 (app): Asked for a referral; Lee submitted it the same afternoon.
22
22