@profullstack/agenticjobs 0.1.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.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/bin/agenticjobs-mcp.mjs +2 -0
- package/bin/agenticjobs.mjs +2 -0
- package/dist/cli/args.d.ts +17 -0
- package/dist/cli/args.js +103 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/format.d.ts +15 -0
- package/dist/cli/format.js +24 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.js +620 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/jobfile.d.ts +15 -0
- package/dist/cli/jobfile.js +94 -0
- package/dist/cli/jobfile.js.map +1 -0
- package/dist/client/client.d.ts +105 -0
- package/dist/client/client.js +182 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/config.d.ts +33 -0
- package/dist/client/config.js +92 -0
- package/dist/client/config.js.map +1 -0
- package/dist/client/fanout.d.ts +35 -0
- package/dist/client/fanout.js +77 -0
- package/dist/client/fanout.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/login.d.ts +23 -0
- package/dist/client/login.js +45 -0
- package/dist/client/login.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/core/applications.d.ts +67 -0
- package/dist/core/applications.js +187 -0
- package/dist/core/applications.js.map +1 -0
- package/dist/core/auth.d.ts +120 -0
- package/dist/core/auth.js +246 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/import.d.ts +52 -0
- package/dist/core/import.js +311 -0
- package/dist/core/import.js.map +1 -0
- package/dist/core/jobs.d.ts +91 -0
- package/dist/core/jobs.js +381 -0
- package/dist/core/jobs.js.map +1 -0
- package/dist/core/orgs.d.ts +21 -0
- package/dist/core/orgs.js +111 -0
- package/dist/core/orgs.js.map +1 -0
- package/dist/core/resumes.d.ts +51 -0
- package/dist/core/resumes.js +144 -0
- package/dist/core/resumes.js.map +1 -0
- package/dist/core/zip.d.ts +17 -0
- package/dist/core/zip.js +77 -0
- package/dist/core/zip.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +15 -0
- package/dist/db/migrate.js +67 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/pool.d.ts +13 -0
- package/dist/db/pool.js +68 -0
- package/dist/db/pool.js.map +1 -0
- package/dist/db/seed.d.ts +9 -0
- package/dist/db/seed.js +120 -0
- package/dist/db/seed.js.map +1 -0
- package/dist/directory/announce.d.ts +32 -0
- package/dist/directory/announce.js +59 -0
- package/dist/directory/announce.js.map +1 -0
- package/dist/directory/federate.d.ts +59 -0
- package/dist/directory/federate.js +126 -0
- package/dist/directory/federate.js.map +1 -0
- package/dist/directory/fetch.d.ts +35 -0
- package/dist/directory/fetch.js +138 -0
- package/dist/directory/fetch.js.map +1 -0
- package/dist/directory/index.d.ts +4 -0
- package/dist/directory/index.js +5 -0
- package/dist/directory/index.js.map +1 -0
- package/dist/directory/registry.d.ts +55 -0
- package/dist/directory/registry.js +155 -0
- package/dist/directory/registry.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/markup/escape.d.ts +20 -0
- package/dist/markup/escape.js +56 -0
- package/dist/markup/escape.js.map +1 -0
- package/dist/markup/markdown.d.ts +35 -0
- package/dist/markup/markdown.js +279 -0
- package/dist/markup/markdown.js.map +1 -0
- package/dist/markup/resume.d.ts +72 -0
- package/dist/markup/resume.js +260 -0
- package/dist/markup/resume.js.map +1 -0
- package/dist/mcp/protocol.d.ts +56 -0
- package/dist/mcp/protocol.js +48 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.d.ts +21 -0
- package/dist/mcp/server.js +73 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +13 -0
- package/dist/mcp/stdio.js +85 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.js +351 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +6 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/instance.d.ts +85 -0
- package/dist/schema/instance.js +147 -0
- package/dist/schema/instance.js.map +1 -0
- package/dist/schema/job.d.ts +151 -0
- package/dist/schema/job.js +73 -0
- package/dist/schema/job.js.map +1 -0
- package/dist/schema/jsonld.d.ts +18 -0
- package/dist/schema/jsonld.js +120 -0
- package/dist/schema/jsonld.js.map +1 -0
- package/dist/schema/query.d.ts +47 -0
- package/dist/schema/query.js +103 -0
- package/dist/schema/query.js.map +1 -0
- package/dist/schema/text.d.ts +27 -0
- package/dist/schema/text.js +117 -0
- package/dist/schema/text.js.map +1 -0
- package/dist/server/app.d.ts +13 -0
- package/dist/server/app.js +57 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/deps.d.ts +20 -0
- package/dist/server/deps.js +2 -0
- package/dist/server/deps.js.map +1 -0
- package/dist/server/middleware.d.ts +39 -0
- package/dist/server/middleware.js +100 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/routes/api.d.ts +20 -0
- package/dist/server/routes/api.js +584 -0
- package/dist/server/routes/api.js.map +1 -0
- package/dist/server/routes/descriptor.d.ts +11 -0
- package/dist/server/routes/descriptor.js +31 -0
- package/dist/server/routes/descriptor.js.map +1 -0
- package/dist/server/routes/discovery.d.ts +12 -0
- package/dist/server/routes/discovery.js +305 -0
- package/dist/server/routes/discovery.js.map +1 -0
- package/dist/server/routes/mcp.d.ts +19 -0
- package/dist/server/routes/mcp.js +85 -0
- package/dist/server/routes/mcp.js.map +1 -0
- package/dist/server/routes/openapi.d.ts +11 -0
- package/dist/server/routes/openapi.js +297 -0
- package/dist/server/routes/openapi.js.map +1 -0
- package/dist/server/routes/pages.d.ts +11 -0
- package/dist/server/routes/pages.js +445 -0
- package/dist/server/routes/pages.js.map +1 -0
- package/dist/server/routes/passkey.d.ts +20 -0
- package/dist/server/routes/passkey.js +198 -0
- package/dist/server/routes/passkey.js.map +1 -0
- package/dist/server/routes/specs.d.ts +10 -0
- package/dist/server/routes/specs.js +42 -0
- package/dist/server/routes/specs.js.map +1 -0
- package/dist/server/serve.d.ts +11 -0
- package/dist/server/serve.js +79 -0
- package/dist/server/serve.js.map +1 -0
- package/dist/tui/index.d.ts +13 -0
- package/dist/tui/index.js +266 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/state.d.ts +63 -0
- package/dist/tui/state.js +136 -0
- package/dist/tui/state.js.map +1 -0
- package/dist/tui/types.d.ts +2 -0
- package/dist/tui/types.js +2 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/tui/views.d.ts +11 -0
- package/dist/tui/views.js +197 -0
- package/dist/tui/views.js.map +1 -0
- package/dist/views/auth.d.ts +21 -0
- package/dist/views/auth.js +5 -0
- package/dist/views/auth.js.map +1 -0
- package/dist/views/docs.d.ts +17 -0
- package/dist/views/docs.js +40 -0
- package/dist/views/docs.js.map +1 -0
- package/dist/views/jobs.d.ts +49 -0
- package/dist/views/jobs.js +44 -0
- package/dist/views/jobs.js.map +1 -0
- package/dist/views/layout.d.ts +58 -0
- package/dist/views/layout.js +36 -0
- package/dist/views/layout.js.map +1 -0
- package/dist/views/me.d.ts +27 -0
- package/dist/views/me.js +6 -0
- package/dist/views/me.js.map +1 -0
- package/dist/views/network.d.ts +21 -0
- package/dist/views/network.js +17 -0
- package/dist/views/network.js.map +1 -0
- package/dist/views/post.d.ts +22 -0
- package/dist/views/post.js +20 -0
- package/dist/views/post.js.map +1 -0
- package/docs/openjob.md +200 -0
- package/docs/openresume.md +143 -0
- package/migrations/0001_init.sql +109 -0
- package/migrations/0002_auth.sql +82 -0
- package/migrations/0003_directory.sql +27 -0
- package/migrations/0004_resumes.sql +63 -0
- package/migrations/0005_application_drafts.sql +20 -0
- package/package.json +92 -0
- package/web/public/app.css +731 -0
- package/web/public/app.js +173 -0
- package/web/public/icon.svg +5 -0
- package/web/public/sw.js +64 -0
- package/web/public/tokens.css +153 -0
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The federation contract.
|
|
3
|
+
*
|
|
4
|
+
* Every instance serves a descriptor at `/.well-known/agenticjobs`, and that
|
|
5
|
+
* one document is how a directory lists it, how a client decides it is talking
|
|
6
|
+
* to a job board at all, and how one instance searches another. Adding a field
|
|
7
|
+
* here is additive; a client must ignore what it does not recognise, because
|
|
8
|
+
* instances upgrade on their owners' schedules and never together.
|
|
9
|
+
*/
|
|
10
|
+
import { clean } from "./text.js";
|
|
11
|
+
/** Bumped only for a change that would break an older reader. */
|
|
12
|
+
export const PROTOCOL_VERSION = 1;
|
|
13
|
+
export const WELL_KNOWN_PATH = '/.well-known/agenticjobs';
|
|
14
|
+
/** How long an instance stays `online` without a successful re-check. */
|
|
15
|
+
export const ONLINE_TTL_MS = 30 * 60 * 1000;
|
|
16
|
+
/** How often an instance re-announces itself. Comfortably inside the TTL. */
|
|
17
|
+
export const HEARTBEAT_MS = 10 * 60 * 1000;
|
|
18
|
+
/** Dropped from the directory after this many consecutive failures. */
|
|
19
|
+
export const MAX_FAILURES = 48;
|
|
20
|
+
export const DEFAULT_DIRECTORY = 'https://agenticjobs.work';
|
|
21
|
+
/**
|
|
22
|
+
* A URL the directory is willing to list.
|
|
23
|
+
*
|
|
24
|
+
* It has to be somewhere another person's browser can actually go. A loopback
|
|
25
|
+
* or link-local address is only reachable from the machine that published it,
|
|
26
|
+
* so listing one produces an entry nobody but its owner can ever open — and,
|
|
27
|
+
* because the directory fetches the URL itself, accepting one would also point
|
|
28
|
+
* the directory's own HTTP client at its own network.
|
|
29
|
+
*/
|
|
30
|
+
export function publishable(raw) {
|
|
31
|
+
let url;
|
|
32
|
+
try {
|
|
33
|
+
url = new URL(raw);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
if (url.protocol !== 'https:' && url.protocol !== 'http:')
|
|
39
|
+
return null;
|
|
40
|
+
const host = url.hostname.replace(/^\[|\]$/g, '').toLowerCase();
|
|
41
|
+
if (host === 'localhost' || host.endsWith('.localhost') || host.endsWith('.local'))
|
|
42
|
+
return null;
|
|
43
|
+
if (host === '::1' || host === '0.0.0.0')
|
|
44
|
+
return null;
|
|
45
|
+
if (/^127\./.test(host))
|
|
46
|
+
return null;
|
|
47
|
+
if (/^10\./.test(host))
|
|
48
|
+
return null;
|
|
49
|
+
if (/^192\.168\./.test(host))
|
|
50
|
+
return null;
|
|
51
|
+
if (/^169\.254\./.test(host))
|
|
52
|
+
return null;
|
|
53
|
+
// 172.16.0.0/12 is the one private range that is easy to get wrong by eye.
|
|
54
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(host))
|
|
55
|
+
return null;
|
|
56
|
+
// Unique local addresses, fc00::/7.
|
|
57
|
+
if (/^f[cd][0-9a-f]{2}:/.test(host))
|
|
58
|
+
return null;
|
|
59
|
+
// Only the origin is ever kept. A path, a query or credentials in the URL
|
|
60
|
+
// would all end up concatenated onto endpoint paths later.
|
|
61
|
+
return new URL(url.origin);
|
|
62
|
+
}
|
|
63
|
+
const MAX_NAME = 80;
|
|
64
|
+
const MAX_TAGLINE = 200;
|
|
65
|
+
/**
|
|
66
|
+
* Validate a descriptor fetched from an instance.
|
|
67
|
+
*
|
|
68
|
+
* Everything here arrived over the network from a stranger, so the URL is
|
|
69
|
+
* taken from where we fetched it rather than from what the document claims,
|
|
70
|
+
* and every string is flattened before it can reach a terminal or a page.
|
|
71
|
+
*/
|
|
72
|
+
export function parseDescriptor(input, fetchedFrom) {
|
|
73
|
+
if (typeof input !== 'object' || input === null)
|
|
74
|
+
return null;
|
|
75
|
+
const record = input;
|
|
76
|
+
const origin = publishable(fetchedFrom);
|
|
77
|
+
if (origin === null)
|
|
78
|
+
return null;
|
|
79
|
+
const protocol = Number(record['protocol']);
|
|
80
|
+
if (!Number.isFinite(protocol) || protocol < 1)
|
|
81
|
+
return null;
|
|
82
|
+
const software = record['software'];
|
|
83
|
+
const softwareRecord = typeof software === 'object' && software !== null ? software : {};
|
|
84
|
+
const jobs = record['jobs'];
|
|
85
|
+
const jobsRecord = typeof jobs === 'object' && jobs !== null ? jobs : {};
|
|
86
|
+
const endpoints = record['endpoints'];
|
|
87
|
+
const endpointsRecord = typeof endpoints === 'object' && endpoints !== null
|
|
88
|
+
? endpoints
|
|
89
|
+
: {};
|
|
90
|
+
const base = origin.origin;
|
|
91
|
+
const contact = clean(record['contact'], 200);
|
|
92
|
+
return {
|
|
93
|
+
protocol: Math.floor(protocol),
|
|
94
|
+
url: base,
|
|
95
|
+
name: clean(record['name'], MAX_NAME) || new URL(base).hostname,
|
|
96
|
+
tagline: clean(record['tagline'], MAX_TAGLINE),
|
|
97
|
+
topics: Array.isArray(record['topics'])
|
|
98
|
+
? record['topics']
|
|
99
|
+
.map((topic) => clean(topic, 40).toLowerCase())
|
|
100
|
+
.filter((topic) => topic !== '')
|
|
101
|
+
.slice(0, 12)
|
|
102
|
+
: [],
|
|
103
|
+
software: {
|
|
104
|
+
name: clean(softwareRecord['name'], 40) || 'unknown',
|
|
105
|
+
version: clean(softwareRecord['version'], 20) || '0',
|
|
106
|
+
},
|
|
107
|
+
jobs: {
|
|
108
|
+
open: count(jobsRecord['open']),
|
|
109
|
+
total: count(jobsRecord['total']),
|
|
110
|
+
},
|
|
111
|
+
endpoints: {
|
|
112
|
+
search: relative(endpointsRecord['search'], base, '/api/v1/jobs'),
|
|
113
|
+
openapi: relative(endpointsRecord['openapi'], base, '/api/v1/openapi.json'),
|
|
114
|
+
mcp: relative(endpointsRecord['mcp'], base, '/api/mcp'),
|
|
115
|
+
feed: relative(endpointsRecord['feed'], base, '/jobs.json'),
|
|
116
|
+
},
|
|
117
|
+
directory: record['directory'] === true,
|
|
118
|
+
contact: contact === '' ? null : contact,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
function count(value) {
|
|
122
|
+
const parsed = Number(value);
|
|
123
|
+
if (!Number.isFinite(parsed) || parsed < 0)
|
|
124
|
+
return 0;
|
|
125
|
+
return Math.min(10_000_000, Math.floor(parsed));
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Endpoints are pinned to the instance's own origin.
|
|
129
|
+
*
|
|
130
|
+
* An instance that advertises `https://example.com/api` for its search
|
|
131
|
+
* endpoint would otherwise have every federated client in the network query
|
|
132
|
+
* example.com on its behalf, with the directory's blessing.
|
|
133
|
+
*/
|
|
134
|
+
function relative(value, base, fallback) {
|
|
135
|
+
if (typeof value !== 'string' || value.trim() === '')
|
|
136
|
+
return `${base}${fallback}`;
|
|
137
|
+
try {
|
|
138
|
+
const resolved = new URL(value, base);
|
|
139
|
+
if (resolved.origin !== base)
|
|
140
|
+
return `${base}${fallback}`;
|
|
141
|
+
return resolved.toString();
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return `${base}${fallback}`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=instance.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"instance.js","sourceRoot":"","sources":["../../src/schema/instance.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a job is, on this board and on every other one.
|
|
3
|
+
*
|
|
4
|
+
* These shapes are the contract between the seven front doors — pages, REST,
|
|
5
|
+
* MCP, CLI, TUI, desktop and the directory — so a field that is not here is a
|
|
6
|
+
* field one surface has and the others do not. They are also the federation
|
|
7
|
+
* contract: an instance reading another instance's listings parses exactly
|
|
8
|
+
* this, which is why the enumerations are string unions with runtime guards
|
|
9
|
+
* rather than TypeScript enums nobody else can see.
|
|
10
|
+
*/
|
|
11
|
+
export declare const EMPLOYMENT_TYPES: readonly ["full-time", "part-time", "contract", "internship", "temporary"];
|
|
12
|
+
export type EmploymentType = (typeof EMPLOYMENT_TYPES)[number];
|
|
13
|
+
export declare const WORKPLACES: readonly ["remote", "hybrid", "onsite"];
|
|
14
|
+
export type Workplace = (typeof WORKPLACES)[number];
|
|
15
|
+
export declare const SENIORITIES: readonly ["intern", "junior", "mid", "senior", "staff", "principal", "lead"];
|
|
16
|
+
export type Seniority = (typeof SENIORITIES)[number];
|
|
17
|
+
export declare const SALARY_PERIODS: readonly ["hour", "day", "week", "month", "year"];
|
|
18
|
+
export type SalaryPeriod = (typeof SALARY_PERIODS)[number];
|
|
19
|
+
export declare const JOB_STATUSES: readonly ["draft", "published", "closed"];
|
|
20
|
+
export type JobStatus = (typeof JOB_STATUSES)[number];
|
|
21
|
+
/**
|
|
22
|
+
* Where an employer stands on candidates who let an agent do the work.
|
|
23
|
+
*
|
|
24
|
+
* This board exists because that question currently gets answered by silent
|
|
25
|
+
* rejection. Making it a required field means a listing has to say, and an
|
|
26
|
+
* agent reading the listing can decide whether to bother.
|
|
27
|
+
*
|
|
28
|
+
* - `welcome` agent-written applications are fine, no disclosure asked.
|
|
29
|
+
* - `disclose` fine, but say so; the application carries an `agent` block.
|
|
30
|
+
* - `human-only` the employer wants a person to have written it.
|
|
31
|
+
*/
|
|
32
|
+
export declare const AGENT_POLICIES: readonly ["welcome", "disclose", "human-only"];
|
|
33
|
+
export type AgentPolicy = (typeof AGENT_POLICIES)[number];
|
|
34
|
+
export interface Salary {
|
|
35
|
+
min: number | null;
|
|
36
|
+
max: number | null;
|
|
37
|
+
currency: string;
|
|
38
|
+
period: SalaryPeriod;
|
|
39
|
+
equity: string | null;
|
|
40
|
+
}
|
|
41
|
+
export interface Organisation {
|
|
42
|
+
id: string;
|
|
43
|
+
slug: string;
|
|
44
|
+
name: string;
|
|
45
|
+
website: string | null;
|
|
46
|
+
logoUrl: string | null;
|
|
47
|
+
description: string | null;
|
|
48
|
+
createdAt: string;
|
|
49
|
+
}
|
|
50
|
+
export interface Job {
|
|
51
|
+
id: string;
|
|
52
|
+
slug: string;
|
|
53
|
+
org: Organisation;
|
|
54
|
+
title: string;
|
|
55
|
+
/** Markdown. Rendered on pages, handed over raw everywhere else. */
|
|
56
|
+
description: string;
|
|
57
|
+
employmentType: EmploymentType;
|
|
58
|
+
workplace: Workplace;
|
|
59
|
+
seniority: Seniority | null;
|
|
60
|
+
/** Free text ("Berlin", "US timezones"). `null` when workplace is remote and unrestricted. */
|
|
61
|
+
location: string | null;
|
|
62
|
+
/** ISO 3166-1 alpha-2 codes a remote hire may sit in. Empty means anywhere. */
|
|
63
|
+
remoteRegions: string[];
|
|
64
|
+
salary: Salary;
|
|
65
|
+
tags: string[];
|
|
66
|
+
stack: string[];
|
|
67
|
+
requirements: string[];
|
|
68
|
+
responsibilities: string[];
|
|
69
|
+
agentPolicy: AgentPolicy;
|
|
70
|
+
apply: ApplyMethod;
|
|
71
|
+
status: JobStatus;
|
|
72
|
+
publishedAt: string | null;
|
|
73
|
+
expiresAt: string | null;
|
|
74
|
+
createdAt: string;
|
|
75
|
+
updatedAt: string;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* How to apply.
|
|
79
|
+
*
|
|
80
|
+
* `board` is the one that matters: it means the application is a POST to this
|
|
81
|
+
* instance against a published schema, which is the only variant an agent can
|
|
82
|
+
* complete without a browser. The other two exist because employers ask for
|
|
83
|
+
* them, and a listing that lies about having them is worse than one that says
|
|
84
|
+
* so plainly.
|
|
85
|
+
*/
|
|
86
|
+
export type ApplyMethod = {
|
|
87
|
+
via: 'board';
|
|
88
|
+
schema: ApplySchema;
|
|
89
|
+
} | {
|
|
90
|
+
via: 'url';
|
|
91
|
+
url: string;
|
|
92
|
+
} | {
|
|
93
|
+
via: 'email';
|
|
94
|
+
email: string;
|
|
95
|
+
};
|
|
96
|
+
/**
|
|
97
|
+
* The shape of an application, published with the job.
|
|
98
|
+
*
|
|
99
|
+
* A subset of JSON Schema on purpose. It is small enough to render as an HTML
|
|
100
|
+
* form and small enough for a model to fill in without a validator, and both
|
|
101
|
+
* of those matter more here than expressiveness.
|
|
102
|
+
*/
|
|
103
|
+
export interface ApplySchema {
|
|
104
|
+
fields: ApplyField[];
|
|
105
|
+
}
|
|
106
|
+
export interface ApplyField {
|
|
107
|
+
name: string;
|
|
108
|
+
label: string;
|
|
109
|
+
type: 'text' | 'textarea' | 'email' | 'url' | 'select' | 'file';
|
|
110
|
+
required: boolean;
|
|
111
|
+
/** Only for `select`. */
|
|
112
|
+
options?: string[];
|
|
113
|
+
/** Shown under the input, and included in the machine-readable schema. */
|
|
114
|
+
help?: string;
|
|
115
|
+
maxLength?: number;
|
|
116
|
+
}
|
|
117
|
+
/** Every job gets these unless the employer replaces them. */
|
|
118
|
+
export declare const DEFAULT_APPLY_SCHEMA: ApplySchema;
|
|
119
|
+
/**
|
|
120
|
+
* `draft` is the candidate-side control point: an agent prepared it and the
|
|
121
|
+
* person has not released it yet. An employer never sees a draft.
|
|
122
|
+
*/
|
|
123
|
+
export declare const APPLICATION_STATUSES: readonly ["draft", "new", "reviewing", "rejected", "hired"];
|
|
124
|
+
export type ApplicationStatus = (typeof APPLICATION_STATUSES)[number];
|
|
125
|
+
export interface Application {
|
|
126
|
+
id: string;
|
|
127
|
+
jobId: string;
|
|
128
|
+
/** Answers keyed by `ApplyField.name`. */
|
|
129
|
+
answers: Record<string, string>;
|
|
130
|
+
/**
|
|
131
|
+
* Present when the applicant declared an agent wrote it. Never inferred:
|
|
132
|
+
* guessing would make the disclosure worthless in both directions.
|
|
133
|
+
*/
|
|
134
|
+
agent: AgentDisclosure | null;
|
|
135
|
+
status: ApplicationStatus;
|
|
136
|
+
/** Null while it is still a draft nobody has sent. */
|
|
137
|
+
submittedAt: string | null;
|
|
138
|
+
createdAt: string;
|
|
139
|
+
}
|
|
140
|
+
export interface AgentDisclosure {
|
|
141
|
+
/** Free text, e.g. "claude-opus-5 via agenticjobs-mcp". */
|
|
142
|
+
name: string;
|
|
143
|
+
/** Whether a human read it before it was sent. */
|
|
144
|
+
supervised: boolean;
|
|
145
|
+
}
|
|
146
|
+
export declare function isEmploymentType(value: unknown): value is EmploymentType;
|
|
147
|
+
export declare function isWorkplace(value: unknown): value is Workplace;
|
|
148
|
+
export declare function isSeniority(value: unknown): value is Seniority;
|
|
149
|
+
export declare function isSalaryPeriod(value: unknown): value is SalaryPeriod;
|
|
150
|
+
export declare function isAgentPolicy(value: unknown): value is AgentPolicy;
|
|
151
|
+
export declare function isJobStatus(value: unknown): value is JobStatus;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What a job is, on this board and on every other one.
|
|
3
|
+
*
|
|
4
|
+
* These shapes are the contract between the seven front doors — pages, REST,
|
|
5
|
+
* MCP, CLI, TUI, desktop and the directory — so a field that is not here is a
|
|
6
|
+
* field one surface has and the others do not. They are also the federation
|
|
7
|
+
* contract: an instance reading another instance's listings parses exactly
|
|
8
|
+
* this, which is why the enumerations are string unions with runtime guards
|
|
9
|
+
* rather than TypeScript enums nobody else can see.
|
|
10
|
+
*/
|
|
11
|
+
export const EMPLOYMENT_TYPES = [
|
|
12
|
+
'full-time',
|
|
13
|
+
'part-time',
|
|
14
|
+
'contract',
|
|
15
|
+
'internship',
|
|
16
|
+
'temporary',
|
|
17
|
+
];
|
|
18
|
+
export const WORKPLACES = ['remote', 'hybrid', 'onsite'];
|
|
19
|
+
export const SENIORITIES = ['intern', 'junior', 'mid', 'senior', 'staff', 'principal', 'lead'];
|
|
20
|
+
export const SALARY_PERIODS = ['hour', 'day', 'week', 'month', 'year'];
|
|
21
|
+
export const JOB_STATUSES = ['draft', 'published', 'closed'];
|
|
22
|
+
/**
|
|
23
|
+
* Where an employer stands on candidates who let an agent do the work.
|
|
24
|
+
*
|
|
25
|
+
* This board exists because that question currently gets answered by silent
|
|
26
|
+
* rejection. Making it a required field means a listing has to say, and an
|
|
27
|
+
* agent reading the listing can decide whether to bother.
|
|
28
|
+
*
|
|
29
|
+
* - `welcome` agent-written applications are fine, no disclosure asked.
|
|
30
|
+
* - `disclose` fine, but say so; the application carries an `agent` block.
|
|
31
|
+
* - `human-only` the employer wants a person to have written it.
|
|
32
|
+
*/
|
|
33
|
+
export const AGENT_POLICIES = ['welcome', 'disclose', 'human-only'];
|
|
34
|
+
/** Every job gets these unless the employer replaces them. */
|
|
35
|
+
export const DEFAULT_APPLY_SCHEMA = {
|
|
36
|
+
fields: [
|
|
37
|
+
{ name: 'name', label: 'Your name', type: 'text', required: true, maxLength: 120 },
|
|
38
|
+
{ name: 'email', label: 'Email', type: 'email', required: true, maxLength: 200 },
|
|
39
|
+
{ name: 'url', label: 'Portfolio, GitHub or LinkedIn', type: 'url', required: false, maxLength: 500 },
|
|
40
|
+
{
|
|
41
|
+
name: 'cover',
|
|
42
|
+
label: 'Why you',
|
|
43
|
+
type: 'textarea',
|
|
44
|
+
required: true,
|
|
45
|
+
maxLength: 5000,
|
|
46
|
+
help: 'Plain text. Short is fine.',
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* `draft` is the candidate-side control point: an agent prepared it and the
|
|
52
|
+
* person has not released it yet. An employer never sees a draft.
|
|
53
|
+
*/
|
|
54
|
+
export const APPLICATION_STATUSES = ['draft', 'new', 'reviewing', 'rejected', 'hired'];
|
|
55
|
+
export function isEmploymentType(value) {
|
|
56
|
+
return typeof value === 'string' && EMPLOYMENT_TYPES.includes(value);
|
|
57
|
+
}
|
|
58
|
+
export function isWorkplace(value) {
|
|
59
|
+
return typeof value === 'string' && WORKPLACES.includes(value);
|
|
60
|
+
}
|
|
61
|
+
export function isSeniority(value) {
|
|
62
|
+
return typeof value === 'string' && SENIORITIES.includes(value);
|
|
63
|
+
}
|
|
64
|
+
export function isSalaryPeriod(value) {
|
|
65
|
+
return typeof value === 'string' && SALARY_PERIODS.includes(value);
|
|
66
|
+
}
|
|
67
|
+
export function isAgentPolicy(value) {
|
|
68
|
+
return typeof value === 'string' && AGENT_POLICIES.includes(value);
|
|
69
|
+
}
|
|
70
|
+
export function isJobStatus(value) {
|
|
71
|
+
return typeof value === 'string' && JOB_STATUSES.includes(value);
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=job.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job.js","sourceRoot":"","sources":["../../src/schema/job.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,MAAM,CAAC,MAAM,gBAAgB,GAAG;IAC9B,WAAW;IACX,WAAW;IACX,UAAU;IACV,YAAY;IACZ,WAAW;CACH,CAAC;AAGX,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,QAAQ,CAAU,CAAC;AAGlE,MAAM,CAAC,MAAM,WAAW,GAAG,CAAC,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,CAAU,CAAC;AAGxG,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,CAAU,CAAC;AAGhF,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAO,EAAE,WAAW,EAAE,QAAQ,CAAU,CAAC;AAGtE;;;;;;;;;;GAUG;AACH,MAAM,CAAC,MAAM,cAAc,GAAG,CAAC,SAAS,EAAE,UAAU,EAAE,YAAY,CAAU,CAAC;AAsF7E,8DAA8D;AAC9D,MAAM,CAAC,MAAM,oBAAoB,GAAgB;IAC/C,MAAM,EAAE;QACN,EAAE,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,WAAW,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE;QAClF,EAAE,IAAI,EAAE,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,SAAS,EAAE,GAAG,EAAE;QAChF,EAAE,IAAI,EAAE,KAAK,EAAE,KAAK,EAAE,+BAA+B,EAAE,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,SAAS,EAAE,GAAG,EAAE;QACrG;YACE,IAAI,EAAE,OAAO;YACb,KAAK,EAAE,SAAS;YAChB,IAAI,EAAE,UAAU;YAChB,QAAQ,EAAE,IAAI;YACd,SAAS,EAAE,IAAI;YACf,IAAI,EAAE,4BAA4B;SACnC;KACF;CACF,CAAC;AAEF;;;GAGG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,OAAO,EAAE,KAAK,EAAE,WAAW,EAAE,UAAU,EAAE,OAAO,CAAU,CAAC;AA0BhG,MAAM,UAAU,gBAAgB,CAAC,KAAc;IAC7C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,gBAAsC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC9F,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,UAAgC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACxF,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,WAAiC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AACzF,CAAC;AACD,MAAM,UAAU,cAAc,CAAC,KAAc;IAC3C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,cAAoC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC5F,CAAC;AACD,MAAM,UAAU,aAAa,CAAC,KAAc;IAC1C,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,cAAoC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC5F,CAAC;AACD,MAAM,UAAU,WAAW,CAAC,KAAc;IACxC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,YAAkC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC1F,CAAC"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema.org JobPosting.
|
|
3
|
+
*
|
|
4
|
+
* Two audiences, one document. Search engines want this because it is the
|
|
5
|
+
* format they already index, and an agent wants it because it is the one job
|
|
6
|
+
* description shape it has certainly seen before. Emitting it on the page and
|
|
7
|
+
* from the API means neither audience needs a bespoke parser for this board.
|
|
8
|
+
*
|
|
9
|
+
* The `agentPolicy` and the application schema have no schema.org equivalent,
|
|
10
|
+
* so they travel in an `additionalProperty` array, which is the vocabulary's
|
|
11
|
+
* own escape hatch and survives every validator.
|
|
12
|
+
*/
|
|
13
|
+
import type { Job } from './job.ts';
|
|
14
|
+
export interface JsonLdJobPosting extends Record<string, unknown> {
|
|
15
|
+
'@context': 'https://schema.org';
|
|
16
|
+
'@type': 'JobPosting';
|
|
17
|
+
}
|
|
18
|
+
export declare function jobPostingJsonLd(job: Job, origin: string): JsonLdJobPosting;
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schema.org JobPosting.
|
|
3
|
+
*
|
|
4
|
+
* Two audiences, one document. Search engines want this because it is the
|
|
5
|
+
* format they already index, and an agent wants it because it is the one job
|
|
6
|
+
* description shape it has certainly seen before. Emitting it on the page and
|
|
7
|
+
* from the API means neither audience needs a bespoke parser for this board.
|
|
8
|
+
*
|
|
9
|
+
* The `agentPolicy` and the application schema have no schema.org equivalent,
|
|
10
|
+
* so they travel in an `additionalProperty` array, which is the vocabulary's
|
|
11
|
+
* own escape hatch and survives every validator.
|
|
12
|
+
*/
|
|
13
|
+
/** Annualise so `baseSalary` is comparable between listings. */
|
|
14
|
+
const PER_YEAR = {
|
|
15
|
+
hour: 2080,
|
|
16
|
+
day: 260,
|
|
17
|
+
week: 52,
|
|
18
|
+
month: 12,
|
|
19
|
+
year: 1,
|
|
20
|
+
};
|
|
21
|
+
export function jobPostingJsonLd(job, origin) {
|
|
22
|
+
const url = `${origin}/jobs/${job.slug}`;
|
|
23
|
+
const doc = {
|
|
24
|
+
'@context': 'https://schema.org',
|
|
25
|
+
'@type': 'JobPosting',
|
|
26
|
+
'@id': url,
|
|
27
|
+
url,
|
|
28
|
+
title: job.title,
|
|
29
|
+
description: job.description,
|
|
30
|
+
datePosted: job.publishedAt ?? job.createdAt,
|
|
31
|
+
employmentType: EMPLOYMENT[job.employmentType] ?? 'OTHER',
|
|
32
|
+
hiringOrganization: {
|
|
33
|
+
'@type': 'Organization',
|
|
34
|
+
name: job.org.name,
|
|
35
|
+
...(job.org.website ? { sameAs: job.org.website } : {}),
|
|
36
|
+
...(job.org.logoUrl ? { logo: job.org.logoUrl } : {}),
|
|
37
|
+
},
|
|
38
|
+
directApply: job.apply.via === 'board',
|
|
39
|
+
};
|
|
40
|
+
if (job.expiresAt)
|
|
41
|
+
doc['validThrough'] = job.expiresAt;
|
|
42
|
+
if (job.tags.length > 0)
|
|
43
|
+
doc['keywords'] = job.tags.join(', ');
|
|
44
|
+
if (job.seniority)
|
|
45
|
+
doc['experienceRequirements'] = job.seniority;
|
|
46
|
+
if (job.requirements.length > 0)
|
|
47
|
+
doc['qualifications'] = job.requirements.join('\n');
|
|
48
|
+
if (job.responsibilities.length > 0)
|
|
49
|
+
doc['responsibilities'] = job.responsibilities.join('\n');
|
|
50
|
+
if (job.workplace === 'remote') {
|
|
51
|
+
// Google's own requirement: TELECOMMUTE plus a location the hire may sit
|
|
52
|
+
// in. Omitting jobLocation on a remote role is the single most common way
|
|
53
|
+
// a correct-looking posting fails validation.
|
|
54
|
+
doc['jobLocationType'] = 'TELECOMMUTE';
|
|
55
|
+
if (job.remoteRegions.length > 0) {
|
|
56
|
+
doc['applicantLocationRequirements'] = job.remoteRegions.map((code) => ({
|
|
57
|
+
'@type': 'Country',
|
|
58
|
+
name: code,
|
|
59
|
+
}));
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (job.location) {
|
|
63
|
+
doc['jobLocation'] = {
|
|
64
|
+
'@type': 'Place',
|
|
65
|
+
address: { '@type': 'PostalAddress', addressLocality: job.location },
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
const salary = job.salary;
|
|
69
|
+
if (salary.min !== null || salary.max !== null) {
|
|
70
|
+
const min = salary.min ?? salary.max;
|
|
71
|
+
const max = salary.max ?? salary.min;
|
|
72
|
+
doc['baseSalary'] = {
|
|
73
|
+
'@type': 'MonetaryAmount',
|
|
74
|
+
currency: salary.currency.toUpperCase(),
|
|
75
|
+
value: {
|
|
76
|
+
'@type': 'QuantitativeValue',
|
|
77
|
+
minValue: min,
|
|
78
|
+
maxValue: max,
|
|
79
|
+
unitText: salary.period.toUpperCase(),
|
|
80
|
+
},
|
|
81
|
+
};
|
|
82
|
+
doc['estimatedSalary'] = {
|
|
83
|
+
'@type': 'MonetaryAmount',
|
|
84
|
+
currency: salary.currency.toUpperCase(),
|
|
85
|
+
value: {
|
|
86
|
+
'@type': 'QuantitativeValue',
|
|
87
|
+
minValue: Math.round((min ?? 0) * (PER_YEAR[salary.period] ?? 1)),
|
|
88
|
+
maxValue: Math.round((max ?? 0) * (PER_YEAR[salary.period] ?? 1)),
|
|
89
|
+
unitText: 'YEAR',
|
|
90
|
+
},
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
const extra = [
|
|
94
|
+
{ '@type': 'PropertyValue', name: 'agentPolicy', value: job.agentPolicy },
|
|
95
|
+
{ '@type': 'PropertyValue', name: 'applyVia', value: job.apply.via },
|
|
96
|
+
];
|
|
97
|
+
if (job.stack.length > 0) {
|
|
98
|
+
extra.push({ '@type': 'PropertyValue', name: 'stack', value: job.stack.join(', ') });
|
|
99
|
+
}
|
|
100
|
+
if (job.apply.via === 'board') {
|
|
101
|
+
// The whole point of the board: an agent can read the form it has to fill
|
|
102
|
+
// in without rendering a page, and the endpoint it posts to is derivable
|
|
103
|
+
// from the @id above.
|
|
104
|
+
extra.push({
|
|
105
|
+
'@type': 'PropertyValue',
|
|
106
|
+
name: 'applySchemaUrl',
|
|
107
|
+
value: `${origin}/api/v1/jobs/${job.slug}/apply-schema`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
doc['additionalProperty'] = extra;
|
|
111
|
+
return doc;
|
|
112
|
+
}
|
|
113
|
+
const EMPLOYMENT = {
|
|
114
|
+
'full-time': 'FULL_TIME',
|
|
115
|
+
'part-time': 'PART_TIME',
|
|
116
|
+
contract: 'CONTRACTOR',
|
|
117
|
+
internship: 'INTERN',
|
|
118
|
+
temporary: 'TEMPORARY',
|
|
119
|
+
};
|
|
120
|
+
//# sourceMappingURL=jsonld.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jsonld.js","sourceRoot":"","sources":["../../src/schema/jsonld.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One query shape, used by the page, the API, MCP, the CLI and the federated
|
|
3
|
+
* search across instances.
|
|
4
|
+
*
|
|
5
|
+
* Federation is the reason this is a declared type rather than whatever
|
|
6
|
+
* querystring each surface felt like parsing: a search fanned out to twelve
|
|
7
|
+
* instances has to mean the same thing on all of them, and an instance running
|
|
8
|
+
* an older release has to be able to ignore a field it does not know without
|
|
9
|
+
* silently returning the wrong set.
|
|
10
|
+
*/
|
|
11
|
+
import { type AgentPolicy, type EmploymentType, type Seniority, type Workplace } from './job.ts';
|
|
12
|
+
export declare const SORTS: readonly ["recent", "relevant", "salary"];
|
|
13
|
+
export type Sort = (typeof SORTS)[number];
|
|
14
|
+
export interface JobQuery {
|
|
15
|
+
/** Full text over title, description, tags and stack. */
|
|
16
|
+
q: string | null;
|
|
17
|
+
employmentType: EmploymentType | null;
|
|
18
|
+
workplace: Workplace | null;
|
|
19
|
+
seniority: Seniority | null;
|
|
20
|
+
agentPolicy: AgentPolicy | null;
|
|
21
|
+
tags: string[];
|
|
22
|
+
/** Minimum annualised salary, in the instance's currency. */
|
|
23
|
+
salaryMin: number | null;
|
|
24
|
+
org: string | null;
|
|
25
|
+
sort: Sort;
|
|
26
|
+
limit: number;
|
|
27
|
+
offset: number;
|
|
28
|
+
}
|
|
29
|
+
export declare const MAX_LIMIT = 100;
|
|
30
|
+
export declare const EMPTY_QUERY: JobQuery;
|
|
31
|
+
/**
|
|
32
|
+
* Parse a querystring into a query.
|
|
33
|
+
*
|
|
34
|
+
* Unknown values are dropped rather than rejected. A filter this instance does
|
|
35
|
+
* not understand must not turn a federated search into an error on one node
|
|
36
|
+
* and a result set on the rest — the user would see a shorter list and no
|
|
37
|
+
* explanation.
|
|
38
|
+
*/
|
|
39
|
+
export declare function parseQuery(params: URLSearchParams): JobQuery;
|
|
40
|
+
/** The inverse, so a client can rebuild a link to the search it just ran. */
|
|
41
|
+
export declare function queryToParams(query: JobQuery): URLSearchParams;
|
|
42
|
+
export interface JobPage<T> {
|
|
43
|
+
items: T[];
|
|
44
|
+
total: number;
|
|
45
|
+
limit: number;
|
|
46
|
+
offset: number;
|
|
47
|
+
}
|