@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.
Files changed (206) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +219 -0
  3. package/bin/agenticjobs-mcp.mjs +2 -0
  4. package/bin/agenticjobs.mjs +2 -0
  5. package/dist/cli/args.d.ts +17 -0
  6. package/dist/cli/args.js +103 -0
  7. package/dist/cli/args.js.map +1 -0
  8. package/dist/cli/format.d.ts +15 -0
  9. package/dist/cli/format.js +24 -0
  10. package/dist/cli/format.js.map +1 -0
  11. package/dist/cli/index.d.ts +13 -0
  12. package/dist/cli/index.js +620 -0
  13. package/dist/cli/index.js.map +1 -0
  14. package/dist/cli/jobfile.d.ts +15 -0
  15. package/dist/cli/jobfile.js +94 -0
  16. package/dist/cli/jobfile.js.map +1 -0
  17. package/dist/client/client.d.ts +105 -0
  18. package/dist/client/client.js +182 -0
  19. package/dist/client/client.js.map +1 -0
  20. package/dist/client/config.d.ts +33 -0
  21. package/dist/client/config.js +92 -0
  22. package/dist/client/config.js.map +1 -0
  23. package/dist/client/fanout.d.ts +35 -0
  24. package/dist/client/fanout.js +77 -0
  25. package/dist/client/fanout.js.map +1 -0
  26. package/dist/client/index.d.ts +4 -0
  27. package/dist/client/index.js +5 -0
  28. package/dist/client/index.js.map +1 -0
  29. package/dist/client/login.d.ts +23 -0
  30. package/dist/client/login.js +45 -0
  31. package/dist/client/login.js.map +1 -0
  32. package/dist/config.d.ts +40 -0
  33. package/dist/config.js +64 -0
  34. package/dist/config.js.map +1 -0
  35. package/dist/core/applications.d.ts +67 -0
  36. package/dist/core/applications.js +187 -0
  37. package/dist/core/applications.js.map +1 -0
  38. package/dist/core/auth.d.ts +120 -0
  39. package/dist/core/auth.js +246 -0
  40. package/dist/core/auth.js.map +1 -0
  41. package/dist/core/import.d.ts +52 -0
  42. package/dist/core/import.js +311 -0
  43. package/dist/core/import.js.map +1 -0
  44. package/dist/core/jobs.d.ts +91 -0
  45. package/dist/core/jobs.js +381 -0
  46. package/dist/core/jobs.js.map +1 -0
  47. package/dist/core/orgs.d.ts +21 -0
  48. package/dist/core/orgs.js +111 -0
  49. package/dist/core/orgs.js.map +1 -0
  50. package/dist/core/resumes.d.ts +51 -0
  51. package/dist/core/resumes.js +144 -0
  52. package/dist/core/resumes.js.map +1 -0
  53. package/dist/core/zip.d.ts +17 -0
  54. package/dist/core/zip.js +77 -0
  55. package/dist/core/zip.js.map +1 -0
  56. package/dist/db/index.d.ts +3 -0
  57. package/dist/db/index.js +3 -0
  58. package/dist/db/index.js.map +1 -0
  59. package/dist/db/migrate.d.ts +15 -0
  60. package/dist/db/migrate.js +67 -0
  61. package/dist/db/migrate.js.map +1 -0
  62. package/dist/db/pool.d.ts +13 -0
  63. package/dist/db/pool.js +68 -0
  64. package/dist/db/pool.js.map +1 -0
  65. package/dist/db/seed.d.ts +9 -0
  66. package/dist/db/seed.js +120 -0
  67. package/dist/db/seed.js.map +1 -0
  68. package/dist/directory/announce.d.ts +32 -0
  69. package/dist/directory/announce.js +59 -0
  70. package/dist/directory/announce.js.map +1 -0
  71. package/dist/directory/federate.d.ts +59 -0
  72. package/dist/directory/federate.js +126 -0
  73. package/dist/directory/federate.js.map +1 -0
  74. package/dist/directory/fetch.d.ts +35 -0
  75. package/dist/directory/fetch.js +138 -0
  76. package/dist/directory/fetch.js.map +1 -0
  77. package/dist/directory/index.d.ts +4 -0
  78. package/dist/directory/index.js +5 -0
  79. package/dist/directory/index.js.map +1 -0
  80. package/dist/directory/registry.d.ts +55 -0
  81. package/dist/directory/registry.js +155 -0
  82. package/dist/directory/registry.js.map +1 -0
  83. package/dist/index.d.ts +14 -0
  84. package/dist/index.js +14 -0
  85. package/dist/index.js.map +1 -0
  86. package/dist/markup/escape.d.ts +20 -0
  87. package/dist/markup/escape.js +56 -0
  88. package/dist/markup/escape.js.map +1 -0
  89. package/dist/markup/markdown.d.ts +35 -0
  90. package/dist/markup/markdown.js +279 -0
  91. package/dist/markup/markdown.js.map +1 -0
  92. package/dist/markup/resume.d.ts +72 -0
  93. package/dist/markup/resume.js +260 -0
  94. package/dist/markup/resume.js.map +1 -0
  95. package/dist/mcp/protocol.d.ts +56 -0
  96. package/dist/mcp/protocol.js +48 -0
  97. package/dist/mcp/protocol.js.map +1 -0
  98. package/dist/mcp/server.d.ts +21 -0
  99. package/dist/mcp/server.js +73 -0
  100. package/dist/mcp/server.js.map +1 -0
  101. package/dist/mcp/stdio.d.ts +13 -0
  102. package/dist/mcp/stdio.js +85 -0
  103. package/dist/mcp/stdio.js.map +1 -0
  104. package/dist/mcp/tools.d.ts +26 -0
  105. package/dist/mcp/tools.js +351 -0
  106. package/dist/mcp/tools.js.map +1 -0
  107. package/dist/schema/index.d.ts +5 -0
  108. package/dist/schema/index.js +6 -0
  109. package/dist/schema/index.js.map +1 -0
  110. package/dist/schema/instance.d.ts +85 -0
  111. package/dist/schema/instance.js +147 -0
  112. package/dist/schema/instance.js.map +1 -0
  113. package/dist/schema/job.d.ts +151 -0
  114. package/dist/schema/job.js +73 -0
  115. package/dist/schema/job.js.map +1 -0
  116. package/dist/schema/jsonld.d.ts +18 -0
  117. package/dist/schema/jsonld.js +120 -0
  118. package/dist/schema/jsonld.js.map +1 -0
  119. package/dist/schema/query.d.ts +47 -0
  120. package/dist/schema/query.js +103 -0
  121. package/dist/schema/query.js.map +1 -0
  122. package/dist/schema/text.d.ts +27 -0
  123. package/dist/schema/text.js +117 -0
  124. package/dist/schema/text.js.map +1 -0
  125. package/dist/server/app.d.ts +13 -0
  126. package/dist/server/app.js +57 -0
  127. package/dist/server/app.js.map +1 -0
  128. package/dist/server/deps.d.ts +20 -0
  129. package/dist/server/deps.js +2 -0
  130. package/dist/server/deps.js.map +1 -0
  131. package/dist/server/middleware.d.ts +39 -0
  132. package/dist/server/middleware.js +100 -0
  133. package/dist/server/middleware.js.map +1 -0
  134. package/dist/server/routes/api.d.ts +20 -0
  135. package/dist/server/routes/api.js +584 -0
  136. package/dist/server/routes/api.js.map +1 -0
  137. package/dist/server/routes/descriptor.d.ts +11 -0
  138. package/dist/server/routes/descriptor.js +31 -0
  139. package/dist/server/routes/descriptor.js.map +1 -0
  140. package/dist/server/routes/discovery.d.ts +12 -0
  141. package/dist/server/routes/discovery.js +305 -0
  142. package/dist/server/routes/discovery.js.map +1 -0
  143. package/dist/server/routes/mcp.d.ts +19 -0
  144. package/dist/server/routes/mcp.js +85 -0
  145. package/dist/server/routes/mcp.js.map +1 -0
  146. package/dist/server/routes/openapi.d.ts +11 -0
  147. package/dist/server/routes/openapi.js +297 -0
  148. package/dist/server/routes/openapi.js.map +1 -0
  149. package/dist/server/routes/pages.d.ts +11 -0
  150. package/dist/server/routes/pages.js +445 -0
  151. package/dist/server/routes/pages.js.map +1 -0
  152. package/dist/server/routes/passkey.d.ts +20 -0
  153. package/dist/server/routes/passkey.js +198 -0
  154. package/dist/server/routes/passkey.js.map +1 -0
  155. package/dist/server/routes/specs.d.ts +10 -0
  156. package/dist/server/routes/specs.js +42 -0
  157. package/dist/server/routes/specs.js.map +1 -0
  158. package/dist/server/serve.d.ts +11 -0
  159. package/dist/server/serve.js +79 -0
  160. package/dist/server/serve.js.map +1 -0
  161. package/dist/tui/index.d.ts +13 -0
  162. package/dist/tui/index.js +266 -0
  163. package/dist/tui/index.js.map +1 -0
  164. package/dist/tui/state.d.ts +63 -0
  165. package/dist/tui/state.js +136 -0
  166. package/dist/tui/state.js.map +1 -0
  167. package/dist/tui/types.d.ts +2 -0
  168. package/dist/tui/types.js +2 -0
  169. package/dist/tui/types.js.map +1 -0
  170. package/dist/tui/views.d.ts +11 -0
  171. package/dist/tui/views.js +197 -0
  172. package/dist/tui/views.js.map +1 -0
  173. package/dist/views/auth.d.ts +21 -0
  174. package/dist/views/auth.js +5 -0
  175. package/dist/views/auth.js.map +1 -0
  176. package/dist/views/docs.d.ts +17 -0
  177. package/dist/views/docs.js +40 -0
  178. package/dist/views/docs.js.map +1 -0
  179. package/dist/views/jobs.d.ts +49 -0
  180. package/dist/views/jobs.js +44 -0
  181. package/dist/views/jobs.js.map +1 -0
  182. package/dist/views/layout.d.ts +58 -0
  183. package/dist/views/layout.js +36 -0
  184. package/dist/views/layout.js.map +1 -0
  185. package/dist/views/me.d.ts +27 -0
  186. package/dist/views/me.js +6 -0
  187. package/dist/views/me.js.map +1 -0
  188. package/dist/views/network.d.ts +21 -0
  189. package/dist/views/network.js +17 -0
  190. package/dist/views/network.js.map +1 -0
  191. package/dist/views/post.d.ts +22 -0
  192. package/dist/views/post.js +20 -0
  193. package/dist/views/post.js.map +1 -0
  194. package/docs/openjob.md +200 -0
  195. package/docs/openresume.md +143 -0
  196. package/migrations/0001_init.sql +109 -0
  197. package/migrations/0002_auth.sql +82 -0
  198. package/migrations/0003_directory.sql +27 -0
  199. package/migrations/0004_resumes.sql +63 -0
  200. package/migrations/0005_application_drafts.sql +20 -0
  201. package/package.json +92 -0
  202. package/web/public/app.css +731 -0
  203. package/web/public/app.js +173 -0
  204. package/web/public/icon.svg +5 -0
  205. package/web/public/sw.js +64 -0
  206. package/web/public/tokens.css +153 -0
@@ -0,0 +1,246 @@
1
+ /**
2
+ * Who someone is, established four ways: a magic link, a passkey, an existing
3
+ * session cookie, or the device flow for anything without a browser.
4
+ *
5
+ * No passwords anywhere. That is the house rule and it is also the reason the
6
+ * device flow exists: a terminal cannot follow a magic link, so it asks the
7
+ * board for a short code instead and a browser approves it.
8
+ */
9
+ import { hashToken, mintToken } from "../config.js";
10
+ import { clean } from "../schema/text.js";
11
+ const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
12
+ const MAGIC_TTL_MS = 15 * 60 * 1000;
13
+ const DEVICE_TTL_MS = 10 * 60 * 1000;
14
+ const CHALLENGE_TTL_MS = 5 * 60 * 1000;
15
+ export const SESSION_COOKIE = 'aj_session';
16
+ export function normaliseEmail(value) {
17
+ const email = clean(value, 200).toLowerCase();
18
+ if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email))
19
+ return null;
20
+ return email;
21
+ }
22
+ /**
23
+ * Find or create the account for an address.
24
+ *
25
+ * The first account on a board becomes its administrator. Any other rule
26
+ * requires a setup step someone has to be told about, and an installer that
27
+ * leaves the operator locked out of their own board is not an installer.
28
+ */
29
+ export async function ensureUser(pool, email, name) {
30
+ const existing = await pool.query(`select id, email, name, is_admin from users where lower(email) = lower($1)`, [email]);
31
+ const found = existing.rows[0];
32
+ if (found !== undefined)
33
+ return found;
34
+ const created = await pool.query(`insert into users (email, name, is_admin)
35
+ values ($1, $2, (select count(*) = 0 from users))
36
+ returning id, email, name, is_admin`, [email, name === undefined ? null : clean(name, 120) || null]);
37
+ const row = created.rows[0];
38
+ if (row === undefined)
39
+ throw new Error('user insert returned no row');
40
+ return row;
41
+ }
42
+ export async function createSession(pool, userId, options = {}) {
43
+ const token = mintToken();
44
+ await pool.query(`insert into sessions (user_id, token_hash, label, via_token, expires_at)
45
+ values ($1, $2, $3, $4, now() + ($5 || ' milliseconds')::interval)`, [
46
+ userId,
47
+ hashToken(token),
48
+ clean(options.label, 80) || 'session',
49
+ options.viaToken === true,
50
+ String(options.ttlMs ?? SESSION_TTL_MS),
51
+ ]);
52
+ return token;
53
+ }
54
+ export async function viewerFromToken(pool, token) {
55
+ if (token.trim() === '')
56
+ return null;
57
+ const result = await pool.query(`select u.id, u.email, u.name, u.is_admin, s.via_token, s.id as session_id
58
+ from sessions s
59
+ join users u on u.id = s.user_id
60
+ where s.token_hash = $1 and s.expires_at > now()
61
+ limit 1`, [hashToken(token)]);
62
+ const row = result.rows[0];
63
+ if (row === undefined)
64
+ return null;
65
+ // Best effort: a failed touch must not cost the caller their request.
66
+ void pool
67
+ .query(`update sessions set last_used_at = now() where id = $1`, [row.session_id])
68
+ .catch(() => undefined);
69
+ return {
70
+ id: row.id,
71
+ email: row.email,
72
+ name: row.name,
73
+ // A device-flow token is never an administrator, and that is enforced HERE
74
+ // rather than only at the routes: the token was typed into a terminal and
75
+ // lives in a file on disk, while a browser session was established in
76
+ // front of the person it belongs to. Reporting `isAdmin: true` for one and
77
+ // relying on every call site to also check `viaToken` is how the third
78
+ // call site gets it wrong.
79
+ isAdmin: row.is_admin && !row.via_token,
80
+ viaToken: row.via_token,
81
+ };
82
+ }
83
+ export async function revokeSession(pool, token) {
84
+ await pool.query(`delete from sessions where token_hash = $1`, [hashToken(token)]);
85
+ }
86
+ /**
87
+ * A device-flow token is never an administrator.
88
+ *
89
+ * The token was typed into a terminal and lives in a file on disk; a browser
90
+ * session was established in front of the person it belongs to. Treating them
91
+ * as equivalent would make every CLI token a full admin credential.
92
+ */
93
+ export function requireAdmin(viewer) {
94
+ return viewer !== null && viewer.isAdmin && !viewer.viaToken;
95
+ }
96
+ /** Only same-origin paths. An open redirect on a login link is a phish. */
97
+ export function safeRedirect(value) {
98
+ const raw = clean(value, 500);
99
+ if (raw === '' || !raw.startsWith('/') || raw.startsWith('//'))
100
+ return null;
101
+ return raw;
102
+ }
103
+ export async function startMagicLink(pool, email, redirect) {
104
+ const token = mintToken();
105
+ const result = await pool.query(`insert into magic_links (token_hash, email, redirect, expires_at)
106
+ values ($1, $2, $3, now() + ($4 || ' milliseconds')::interval)
107
+ returning expires_at`, [hashToken(token), email, redirect, String(MAGIC_TTL_MS)]);
108
+ return { token, expiresAt: result.rows[0]?.expires_at ?? new Date().toISOString() };
109
+ }
110
+ /**
111
+ * Spend a magic link.
112
+ *
113
+ * The update is what claims it, not a read followed by a write: two clicks
114
+ * arriving together (a mail client prefetching the link, then the person
115
+ * clicking it) would both pass a read-then-write check.
116
+ */
117
+ export async function consumeMagicLink(pool, token) {
118
+ const claimed = await pool.query(`update magic_links
119
+ set used_at = now()
120
+ where token_hash = $1 and used_at is null and expires_at > now()
121
+ returning email, redirect`, [hashToken(token)]);
122
+ const row = claimed.rows[0];
123
+ if (row === undefined)
124
+ return null;
125
+ const user = await ensureUser(pool, row.email);
126
+ return { user, redirect: row.redirect };
127
+ }
128
+ /** No characters that look like each other when read off a screen. */
129
+ const CODE_ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZ23456789';
130
+ function userCode() {
131
+ const bytes = new Uint8Array(8);
132
+ crypto.getRandomValues(bytes);
133
+ let out = '';
134
+ for (let index = 0; index < bytes.length; index += 1) {
135
+ if (index === 4)
136
+ out += '-';
137
+ out += CODE_ALPHABET[(bytes[index] ?? 0) % CODE_ALPHABET.length];
138
+ }
139
+ return out;
140
+ }
141
+ export async function startDeviceAuth(pool, label, publicUrl) {
142
+ const deviceCode = mintToken();
143
+ const code = userCode();
144
+ const result = await pool.query(`insert into device_codes (device_code_hash, user_code, label, expires_at)
145
+ values ($1, $2, $3, now() + ($4 || ' milliseconds')::interval)
146
+ returning expires_at`, [hashToken(deviceCode), code, clean(label, 80) || 'terminal', String(DEVICE_TTL_MS)]);
147
+ return {
148
+ deviceCode,
149
+ userCode: code,
150
+ verifyUrl: `${publicUrl}/device`,
151
+ interval: 2,
152
+ expiresAt: Date.parse(result.rows[0]?.expires_at ?? '') || Date.now() + DEVICE_TTL_MS,
153
+ };
154
+ }
155
+ /**
156
+ * Collect the token for an approved code.
157
+ *
158
+ * The token is cleared in the same statement that returns it, so it is handed
159
+ * over exactly once: a second poll with the same device code gets nothing,
160
+ * which is what makes a leaked poll response useless.
161
+ *
162
+ * An unknown or expired code reports `expired` rather than `pending`. A client
163
+ * told `pending` for a code the server has forgotten polls until its own
164
+ * timeout with no way to tell that it is waiting for nothing.
165
+ */
166
+ export async function pollDeviceAuth(pool, deviceCode) {
167
+ const hash = hashToken(deviceCode);
168
+ // The token has to be read as it was BEFORE the update, and Postgres
169
+ // RETURNING gives post-update values - so a plain
170
+ // `update ... set token = null ... returning token` hands back the null it
171
+ // just wrote and the terminal waits forever. The CTE holds the old row (and
172
+ // locks it, so two polls cannot both claim), and RETURNING reads from that.
173
+ const claimed = await pool.query(`with claimed as (
174
+ select device_code_hash, token
175
+ from device_codes
176
+ where device_code_hash = $1 and status = 'approved' and token is not null
177
+ for update
178
+ )
179
+ update device_codes d
180
+ set token = null, status = 'collected'
181
+ from claimed
182
+ where d.device_code_hash = claimed.device_code_hash
183
+ returning claimed.token`, [hash]);
184
+ const token = claimed.rows[0]?.token;
185
+ if (typeof token === 'string')
186
+ return { status: 'approved', token };
187
+ const pending = await pool.query(`select status from device_codes
188
+ where device_code_hash = $1 and expires_at > now() and status = 'pending'`, [hash]);
189
+ return pending.rows.length > 0 ? { status: 'pending' } : { status: 'expired' };
190
+ }
191
+ export async function findDeviceCode(pool, code) {
192
+ const result = await pool.query(`select user_code, label from device_codes
193
+ where user_code = $1 and status = 'pending' and expires_at > now()`, [code.trim().toUpperCase()]);
194
+ const row = result.rows[0];
195
+ return row === undefined ? null : { userCode: row.user_code, label: row.label };
196
+ }
197
+ export async function approveDeviceCode(pool, code, userId) {
198
+ const label = await findDeviceCode(pool, code);
199
+ if (label === null)
200
+ return false;
201
+ // The terminal's token is marked via_token, which is what keeps it out of
202
+ // the admin routes for the rest of its life.
203
+ const token = await createSession(pool, userId, {
204
+ label: `terminal: ${label.label}`,
205
+ viaToken: true,
206
+ });
207
+ const updated = await pool.query(`update device_codes
208
+ set status = 'approved', user_id = $2, token = $3
209
+ where user_code = $1 and status = 'pending' and expires_at > now()`, [code.trim().toUpperCase(), userId, token]);
210
+ return (updated.rowCount ?? 0) > 0;
211
+ }
212
+ // --- webauthn challenges ------------------------------------------------
213
+ export async function storeChallenge(pool, challenge, purpose, who) {
214
+ await pool.query(`insert into webauthn_challenges (challenge, email, user_id, purpose, expires_at)
215
+ values ($1, $2, $3, $4, now() + ($5 || ' milliseconds')::interval)
216
+ on conflict (challenge) do nothing`, [
217
+ challenge,
218
+ who.email ?? null,
219
+ who.userId ?? null,
220
+ purpose,
221
+ String(CHALLENGE_TTL_MS),
222
+ ]);
223
+ }
224
+ /** Claimed on read: a challenge is single use by definition. */
225
+ export async function takeChallenge(pool, challenge) {
226
+ const result = await pool.query(`delete from webauthn_challenges
227
+ where challenge = $1 and expires_at > now()
228
+ returning challenge, email, user_id, purpose`, [challenge]);
229
+ const row = result.rows[0];
230
+ if (row === undefined)
231
+ return null;
232
+ return {
233
+ challenge: row.challenge,
234
+ email: row.email,
235
+ userId: row.user_id,
236
+ purpose: row.purpose,
237
+ };
238
+ }
239
+ /** Swept opportunistically rather than on a timer, so there is no worker. */
240
+ export async function sweepExpired(pool) {
241
+ await pool.query(`delete from sessions where expires_at < now()`);
242
+ await pool.query(`delete from magic_links where expires_at < now()`);
243
+ await pool.query(`delete from device_codes where expires_at < now() - interval '1 hour'`);
244
+ await pool.query(`delete from webauthn_challenges where expires_at < now()`);
245
+ }
246
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../../src/core/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Turning whatever a candidate has into Markdown.
3
+ *
4
+ * The rule the rest of the board depends on: after this module runs there is
5
+ * exactly one representation of a resume, and it is Markdown the candidate can
6
+ * read and edit. An upload is an import step, not a storage format.
7
+ *
8
+ * Four inputs, in descending order of how well they survive the trip:
9
+ *
10
+ * .md used as-is; nothing is inferred and nothing is lost
11
+ * .txt structure guessed from blank lines and capitalisation, then edited
12
+ * .docx read directly here, because a .docx is a ZIP with one XML in it
13
+ * .pdf handed to pdftotext, because laying text back out of a PDF is a
14
+ * genuinely hard problem and poppler has already solved it
15
+ *
16
+ * External tools are used when they are installed and their absence is
17
+ * reported as a sentence a person can act on, never as a stack trace. The
18
+ * container image installs both, so this only degrades on a bare laptop.
19
+ */
20
+ export declare const MAX_UPLOAD_BYTES: number;
21
+ export interface ImportResult {
22
+ markdown: string;
23
+ /** How it got here, for the "converted from your PDF" line on the editor. */
24
+ via: 'markdown' | 'text' | 'docx' | 'pdf' | 'pandoc';
25
+ warnings: string[];
26
+ }
27
+ export declare class ImportProblem extends Error {
28
+ }
29
+ export declare function extensionOf(filename: string): string;
30
+ export declare function importDocument(filename: string, bytes: Buffer, mime?: string): Promise<ImportResult>;
31
+ /**
32
+ * Plain text to Markdown.
33
+ *
34
+ * Deliberately conservative. Only two things are inferred - a line that is
35
+ * short, standalone and title-cased or upper-case becomes a heading, and a
36
+ * line already starting with a bullet character becomes a list item - because
37
+ * the candidate is about to see the result in an editor and a wrong guess
38
+ * costs them more than a missing one.
39
+ */
40
+ export declare function textToMarkdown(source: string): string;
41
+ /**
42
+ * .docx to Markdown, without a library.
43
+ *
44
+ * Word's XML is verbose but the parts that matter are few: a paragraph carries
45
+ * a style name that says whether it is a heading or a list item, and its text
46
+ * is the concatenation of the runs inside it. Bold and italic runs are mapped;
47
+ * everything else - fonts, colours, tracked changes - is dropped on purpose,
48
+ * because none of it survives into a Markdown resume anyway.
49
+ */
50
+ export declare function docxToMarkdown(bytes: Buffer): Promise<ImportResult>;
51
+ /** Laying text back out of a PDF is poppler's job, not ours. */
52
+ export declare function pdfToMarkdown(bytes: Buffer): Promise<ImportResult>;
@@ -0,0 +1,311 @@
1
+ /**
2
+ * Turning whatever a candidate has into Markdown.
3
+ *
4
+ * The rule the rest of the board depends on: after this module runs there is
5
+ * exactly one representation of a resume, and it is Markdown the candidate can
6
+ * read and edit. An upload is an import step, not a storage format.
7
+ *
8
+ * Four inputs, in descending order of how well they survive the trip:
9
+ *
10
+ * .md used as-is; nothing is inferred and nothing is lost
11
+ * .txt structure guessed from blank lines and capitalisation, then edited
12
+ * .docx read directly here, because a .docx is a ZIP with one XML in it
13
+ * .pdf handed to pdftotext, because laying text back out of a PDF is a
14
+ * genuinely hard problem and poppler has already solved it
15
+ *
16
+ * External tools are used when they are installed and their absence is
17
+ * reported as a sentence a person can act on, never as a stack trace. The
18
+ * container image installs both, so this only degrades on a bare laptop.
19
+ */
20
+ import { execFile } from 'node:child_process';
21
+ import { mkdtemp, rm, writeFile } from 'node:fs/promises';
22
+ import { tmpdir } from 'node:os';
23
+ import { join } from 'node:path';
24
+ import { promisify } from 'node:util';
25
+ import { readZipEntry, ZipProblem } from "./zip.js";
26
+ const run = promisify(execFile);
27
+ export const MAX_UPLOAD_BYTES = 8 * 1024 * 1024;
28
+ export class ImportProblem extends Error {
29
+ }
30
+ export function extensionOf(filename) {
31
+ const match = /\.([A-Za-z0-9]+)$/.exec(filename.trim());
32
+ return (match?.[1] ?? '').toLowerCase();
33
+ }
34
+ export async function importDocument(filename, bytes, mime = '') {
35
+ if (bytes.length === 0)
36
+ throw new ImportProblem('That file is empty.');
37
+ if (bytes.length > MAX_UPLOAD_BYTES) {
38
+ throw new ImportProblem(`That file is ${Math.round(bytes.length / 1024 / 1024)}MB. The limit is ${MAX_UPLOAD_BYTES / 1024 / 1024}MB.`);
39
+ }
40
+ const extension = extensionOf(filename);
41
+ const kind = classify(extension, mime, bytes);
42
+ switch (kind) {
43
+ case 'markdown':
44
+ return { markdown: decodeText(bytes).trim(), via: 'markdown', warnings: [] };
45
+ case 'text':
46
+ return { markdown: textToMarkdown(decodeText(bytes)), via: 'text', warnings: [] };
47
+ case 'docx':
48
+ return docxToMarkdown(bytes);
49
+ case 'pdf':
50
+ return pdfToMarkdown(bytes);
51
+ default:
52
+ return legacyToMarkdown(bytes, extension);
53
+ }
54
+ }
55
+ function classify(extension, mime, bytes) {
56
+ // The magic bytes win over the extension. A resume renamed from .pdf to
57
+ // .docx is a mistake someone makes once, and the file itself is not lying.
58
+ if (bytes.subarray(0, 5).toString('latin1') === '%PDF-')
59
+ return 'pdf';
60
+ if (bytes.subarray(0, 2).toString('latin1') === 'PK' && (extension === 'docx' || mime.includes('wordprocessingml'))) {
61
+ return 'docx';
62
+ }
63
+ if (['md', 'markdown', 'mdown', 'mkd'].includes(extension))
64
+ return 'markdown';
65
+ if (extension === 'txt' || mime === 'text/plain')
66
+ return 'text';
67
+ if (extension === 'docx')
68
+ return 'docx';
69
+ if (extension === 'pdf')
70
+ return 'pdf';
71
+ if (['doc', 'odt', 'rtf', 'html', 'htm'].includes(extension))
72
+ return 'legacy';
73
+ // Anything that decodes cleanly as UTF-8 is treated as text rather than
74
+ // refused: a resume with no extension is still a resume.
75
+ return looksTextual(bytes) ? 'text' : 'legacy';
76
+ }
77
+ function looksTextual(bytes) {
78
+ const sample = bytes.subarray(0, 4096);
79
+ let control = 0;
80
+ for (const byte of sample) {
81
+ if (byte === 0)
82
+ return false;
83
+ if (byte < 9 || (byte > 13 && byte < 32))
84
+ control += 1;
85
+ }
86
+ return control / Math.max(1, sample.length) < 0.02;
87
+ }
88
+ function decodeText(bytes) {
89
+ // A BOM left in place shows up as a stray character in the first heading.
90
+ const text = bytes.toString('utf8');
91
+ return text.charCodeAt(0) === 0xfeff ? text.slice(1) : text;
92
+ }
93
+ /**
94
+ * Plain text to Markdown.
95
+ *
96
+ * Deliberately conservative. Only two things are inferred - a line that is
97
+ * short, standalone and title-cased or upper-case becomes a heading, and a
98
+ * line already starting with a bullet character becomes a list item - because
99
+ * the candidate is about to see the result in an editor and a wrong guess
100
+ * costs them more than a missing one.
101
+ */
102
+ export function textToMarkdown(source) {
103
+ const lines = source.replace(/\r\n?/g, '\n').split('\n');
104
+ const out = [];
105
+ let first = true;
106
+ for (let index = 0; index < lines.length; index += 1) {
107
+ const line = (lines[index] ?? '').trimEnd();
108
+ const trimmed = line.trim();
109
+ if (trimmed === '') {
110
+ out.push('');
111
+ continue;
112
+ }
113
+ const bullet = /^[•·▪●*+-]\s+(.*)$/.exec(trimmed);
114
+ if (bullet !== null) {
115
+ out.push(`- ${bullet[1] ?? ''}`);
116
+ continue;
117
+ }
118
+ if (first) {
119
+ out.push(`# ${trimmed}`);
120
+ first = false;
121
+ continue;
122
+ }
123
+ const previousBlank = (lines[index - 1] ?? '').trim() === '';
124
+ const nextBlank = (lines[index + 1] ?? '').trim() === '';
125
+ const heading = previousBlank &&
126
+ nextBlank &&
127
+ trimmed.length <= 60 &&
128
+ !trimmed.endsWith('.') &&
129
+ (trimmed === trimmed.toUpperCase() || /^[A-Z][a-z]+(\s+[A-Za-z&/]+){0,4}$/.test(trimmed));
130
+ out.push(heading ? `## ${titleCase(trimmed)}` : trimmed);
131
+ }
132
+ return collapseBlankRuns(out.join('\n')).trim();
133
+ }
134
+ function titleCase(value) {
135
+ if (value !== value.toUpperCase())
136
+ return value;
137
+ return value
138
+ .toLowerCase()
139
+ .replace(/\b([a-z])/g, (character) => character.toUpperCase());
140
+ }
141
+ function collapseBlankRuns(value) {
142
+ return value.replace(/\n{3,}/g, '\n\n');
143
+ }
144
+ /**
145
+ * .docx to Markdown, without a library.
146
+ *
147
+ * Word's XML is verbose but the parts that matter are few: a paragraph carries
148
+ * a style name that says whether it is a heading or a list item, and its text
149
+ * is the concatenation of the runs inside it. Bold and italic runs are mapped;
150
+ * everything else - fonts, colours, tracked changes - is dropped on purpose,
151
+ * because none of it survives into a Markdown resume anyway.
152
+ */
153
+ export async function docxToMarkdown(bytes) {
154
+ const warnings = [];
155
+ let xml;
156
+ try {
157
+ xml = readZipEntry(bytes, 'word/document.xml');
158
+ }
159
+ catch (error) {
160
+ if (error instanceof ZipProblem)
161
+ throw new ImportProblem(error.message);
162
+ throw error;
163
+ }
164
+ if (xml === null) {
165
+ throw new ImportProblem('That .docx has no word/document.xml inside it.');
166
+ }
167
+ const document = xml.toString('utf8');
168
+ const out = [];
169
+ const paragraphs = document.match(/<w:p[ >][\s\S]*?<\/w:p>|<w:p\/>/g) ?? [];
170
+ for (const paragraph of paragraphs) {
171
+ const style = /<w:pStyle[^>]*w:val="([^"]+)"/.exec(paragraph)?.[1] ?? '';
172
+ const isList = /<w:numPr[ >]/.test(paragraph);
173
+ const text = runsToText(paragraph).trim();
174
+ if (text === '') {
175
+ out.push('');
176
+ continue;
177
+ }
178
+ const headingLevel = /^Heading(\d)$/i.exec(style)?.[1];
179
+ if (headingLevel !== undefined) {
180
+ out.push('', `${'#'.repeat(Math.min(6, Number(headingLevel)))} ${stripMarks(text)}`, '');
181
+ continue;
182
+ }
183
+ if (/^Title$/i.test(style)) {
184
+ out.push('', `# ${stripMarks(text)}`, '');
185
+ continue;
186
+ }
187
+ if (isList) {
188
+ out.push(`- ${text}`);
189
+ continue;
190
+ }
191
+ out.push(text);
192
+ }
193
+ const tables = (document.match(/<w:tbl[ >]/g) ?? []).length;
194
+ if (tables > 0) {
195
+ // Word tables are commonly used purely for two-column layout, so turning
196
+ // them into Markdown tables produces something worse than flat text.
197
+ warnings.push(`${tables} table${tables === 1 ? '' : 's'} in the document were flattened to plain lines. Check the layout.`);
198
+ }
199
+ const markdown = collapseBlankRuns(out.join('\n')).trim();
200
+ if (markdown === '')
201
+ throw new ImportProblem('No text could be read out of that .docx.');
202
+ return { markdown, via: 'docx', warnings };
203
+ }
204
+ function runsToText(paragraph) {
205
+ let out = '';
206
+ const runs = paragraph.match(/<w:r[ >][\s\S]*?<\/w:r>/g) ?? [];
207
+ for (const run of runs) {
208
+ const pieces = run.match(/<w:t(?:\s[^>]*)?>([\s\S]*?)<\/w:t>/g) ?? [];
209
+ let text = pieces
210
+ .map((piece) => decodeXml(piece.replace(/<[^>]+>/g, '')))
211
+ .join('');
212
+ if (text === '') {
213
+ // A tab or an explicit break carries meaning even with no text.
214
+ if (/<w:tab\b/.test(run))
215
+ out += ' ';
216
+ if (/<w:br\b/.test(run))
217
+ out += '\n';
218
+ continue;
219
+ }
220
+ if (/<w:b\/>|<w:b\s[^>]*w:val="(?:1|true|on)"/.test(run))
221
+ text = `**${text}**`;
222
+ if (/<w:i\/>|<w:i\s[^>]*w:val="(?:1|true|on)"/.test(run))
223
+ text = `*${text}*`;
224
+ out += text;
225
+ }
226
+ return out;
227
+ }
228
+ function stripMarks(value) {
229
+ return value.replace(/\*+/g, '').trim();
230
+ }
231
+ function decodeXml(value) {
232
+ return value
233
+ .replace(/&lt;/g, '<')
234
+ .replace(/&gt;/g, '>')
235
+ .replace(/&quot;/g, '"')
236
+ .replace(/&apos;/g, "'")
237
+ .replace(/&#(\d+);/g, (_whole, code) => String.fromCodePoint(Number(code)))
238
+ .replace(/&amp;/g, '&');
239
+ }
240
+ /** Laying text back out of a PDF is poppler's job, not ours. */
241
+ export async function pdfToMarkdown(bytes) {
242
+ const text = await withTempFile(bytes, 'pdf', async (path) => {
243
+ try {
244
+ // -layout keeps columns apart, which is the difference between a
245
+ // readable resume and every line of a two-column CV interleaved.
246
+ const { stdout } = await run('pdftotext', ['-layout', '-nopgbrk', '-enc', 'UTF-8', path, '-'], {
247
+ maxBuffer: 16 * 1024 * 1024,
248
+ });
249
+ return stdout;
250
+ }
251
+ catch (error) {
252
+ if (isMissingBinary(error)) {
253
+ throw new ImportProblem('This board cannot read PDFs because pdftotext is not installed. Install poppler-utils, or upload a .md, .txt or .docx instead.');
254
+ }
255
+ throw new ImportProblem(`pdftotext could not read that PDF: ${error instanceof Error ? error.message : String(error)}`);
256
+ }
257
+ });
258
+ const markdown = textToMarkdown(text);
259
+ if (markdown.replace(/[#\s-]/g, '') === '') {
260
+ throw new ImportProblem('That PDF has no extractable text. It is probably a scan; upload a .md, .txt or .docx instead.');
261
+ }
262
+ return {
263
+ markdown,
264
+ via: 'pdf',
265
+ warnings: [
266
+ 'Converted from a PDF, so the structure is a guess. Check the headings before you save.',
267
+ ],
268
+ };
269
+ }
270
+ /** .doc, .odt, .rtf and HTML, if pandoc happens to be installed. */
271
+ async function legacyToMarkdown(bytes, extension) {
272
+ return withTempFile(bytes, extension || 'bin', async (path) => {
273
+ try {
274
+ const { stdout } = await run('pandoc', ['--from', pandocFormat(extension), '--to', 'gfm', '--wrap=none', path], { maxBuffer: 16 * 1024 * 1024 });
275
+ return { markdown: collapseBlankRuns(stdout).trim(), via: 'pandoc', warnings: [] };
276
+ }
277
+ catch (error) {
278
+ if (isMissingBinary(error)) {
279
+ throw new ImportProblem(`This board cannot read .${extension} files because pandoc is not installed. Upload a .md, .txt, .docx or .pdf instead.`);
280
+ }
281
+ throw new ImportProblem(`That .${extension} could not be converted: ${error instanceof Error ? error.message : String(error)}`);
282
+ }
283
+ });
284
+ }
285
+ function pandocFormat(extension) {
286
+ if (extension === 'odt')
287
+ return 'odt';
288
+ if (extension === 'rtf')
289
+ return 'rtf';
290
+ if (extension === 'html' || extension === 'htm')
291
+ return 'html';
292
+ return 'doc';
293
+ }
294
+ function isMissingBinary(error) {
295
+ return (typeof error === 'object' &&
296
+ error !== null &&
297
+ 'code' in error &&
298
+ error.code === 'ENOENT');
299
+ }
300
+ async function withTempFile(bytes, extension, work) {
301
+ const directory = await mkdtemp(join(tmpdir(), 'agenticjobs-'));
302
+ const path = join(directory, `upload.${extension}`);
303
+ try {
304
+ await writeFile(path, bytes);
305
+ return await work(path);
306
+ }
307
+ finally {
308
+ await rm(directory, { recursive: true, force: true }).catch(() => undefined);
309
+ }
310
+ }
311
+ //# sourceMappingURL=import.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"import.js","sourceRoot":"","sources":["../../src/core/import.ts"],"names":[],"mappings":"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"}