@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,51 @@
1
+ /**
2
+ * Resume CRUD.
3
+ *
4
+ * The Markdown column is the only thing anyone writes. `parsed` and `search`
5
+ * are both recomputed from it on every save - `parsed` here, `search` by the
6
+ * trigger - so there is no path by which the structured view and the document
7
+ * can disagree.
8
+ */
9
+ import type pg from 'pg';
10
+ import { type OpenResume } from '../markup/resume.ts';
11
+ export declare const VISIBILITIES: readonly ["private", "link", "public"];
12
+ export type Visibility = (typeof VISIBILITIES)[number];
13
+ export declare function isVisibility(value: unknown): value is Visibility;
14
+ export interface Resume {
15
+ id: string;
16
+ userId: string;
17
+ slug: string;
18
+ title: string;
19
+ markdown: string;
20
+ parsed: OpenResume | null;
21
+ visibility: Visibility;
22
+ sourceName: string | null;
23
+ createdAt: string;
24
+ updatedAt: string;
25
+ }
26
+ export declare function listResumes(pool: pg.Pool, userId: string): Promise<Resume[]>;
27
+ export declare function getResume(pool: pg.Pool, userId: string, slug: string): Promise<Resume | null>;
28
+ export declare function getResumeById(pool: pg.Pool, id: string): Promise<Resume | null>;
29
+ /** For a shared link: only resumes the owner has opened up. */
30
+ export declare function getSharedResume(pool: pg.Pool, id: string): Promise<Resume | null>;
31
+ export interface SaveResume {
32
+ title?: string;
33
+ markdown: string;
34
+ visibility?: Visibility;
35
+ source?: {
36
+ name: string;
37
+ mime: string;
38
+ bytes: Buffer;
39
+ } | null;
40
+ }
41
+ export declare function createResume(pool: pg.Pool, userId: string, input: SaveResume): Promise<Resume>;
42
+ export declare function updateResume(pool: pg.Pool, userId: string, slug: string, input: SaveResume): Promise<Resume | null>;
43
+ export declare function deleteResume(pool: pg.Pool, userId: string, slug: string): Promise<boolean>;
44
+ /** The original upload, for re-converting a document that came out badly. */
45
+ export declare function getResumeSource(pool: pg.Pool, userId: string, slug: string): Promise<{
46
+ name: string;
47
+ mime: string;
48
+ bytes: Buffer;
49
+ } | null>;
50
+ /** A first resume, so the editor is never an empty box. */
51
+ export declare function ensureFirstResume(pool: pg.Pool, userId: string, name: string | null): Promise<Resume>;
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Resume CRUD.
3
+ *
4
+ * The Markdown column is the only thing anyone writes. `parsed` and `search`
5
+ * are both recomputed from it on every save - `parsed` here, `search` by the
6
+ * trigger - so there is no path by which the structured view and the document
7
+ * can disagree.
8
+ */
9
+ import { parseResume, resumeTemplate } from "../markup/resume.js";
10
+ import { clean, slugify, suffix } from "../schema/text.js";
11
+ export const VISIBILITIES = ['private', 'link', 'public'];
12
+ export function isVisibility(value) {
13
+ return typeof value === 'string' && VISIBILITIES.includes(value);
14
+ }
15
+ function toResume(row) {
16
+ return {
17
+ id: row.id,
18
+ userId: row.user_id,
19
+ slug: row.slug,
20
+ title: row.title,
21
+ markdown: row.markdown,
22
+ parsed: row.parsed,
23
+ visibility: isVisibility(row.visibility) ? row.visibility : 'private',
24
+ sourceName: row.source_name,
25
+ createdAt: row.created_at,
26
+ updatedAt: row.updated_at,
27
+ };
28
+ }
29
+ // The source bytes are never in the default projection: they are megabytes of
30
+ // PDF that no page needs, and selecting them by habit is how a list endpoint
31
+ // starts moving 40MB.
32
+ const SELECT = `select id, user_id, slug, title, markdown, parsed, visibility, source_name,
33
+ created_at, updated_at from resumes`;
34
+ export async function listResumes(pool, userId) {
35
+ const result = await pool.query(`${SELECT} where user_id = $1 order by updated_at desc`, [userId]);
36
+ return result.rows.map(toResume);
37
+ }
38
+ export async function getResume(pool, userId, slug) {
39
+ const result = await pool.query(`${SELECT} where user_id = $1 and slug = $2`, [
40
+ userId,
41
+ slug,
42
+ ]);
43
+ const row = result.rows[0];
44
+ return row === undefined ? null : toResume(row);
45
+ }
46
+ export async function getResumeById(pool, id) {
47
+ const result = await pool.query(`${SELECT} where id = $1`, [id]);
48
+ const row = result.rows[0];
49
+ return row === undefined ? null : toResume(row);
50
+ }
51
+ /** For a shared link: only resumes the owner has opened up. */
52
+ export async function getSharedResume(pool, id) {
53
+ const result = await pool.query(`${SELECT} where id = $1 and visibility in ('link', 'public')`, [id]);
54
+ const row = result.rows[0];
55
+ return row === undefined ? null : toResume(row);
56
+ }
57
+ async function uniqueSlug(pool, userId, title) {
58
+ const base = slugify(title);
59
+ for (let attempt = 0; attempt < 6; attempt += 1) {
60
+ const candidate = attempt === 0 ? base : `${base}-${suffix(4)}`;
61
+ const taken = await pool.query(`select 1 from resumes where user_id = $1 and slug = $2`, [
62
+ userId,
63
+ candidate,
64
+ ]);
65
+ if (taken.rows.length === 0)
66
+ return candidate;
67
+ }
68
+ return `${base}-${Date.now().toString(36)}`;
69
+ }
70
+ export async function createResume(pool, userId, input) {
71
+ const markdown = clean(input.markdown, 200_000);
72
+ const parsed = parseResume(markdown);
73
+ // The document's own h1 is a better title than "Resume", and it is what the
74
+ // candidate would have typed anyway.
75
+ const title = clean(input.title, 120) || parsed.name || 'Resume';
76
+ const slug = await uniqueSlug(pool, userId, title);
77
+ const result = await pool.query(`insert into resumes (user_id, slug, title, markdown, parsed, visibility,
78
+ source_name, source_mime, source_bytes)
79
+ values ($1, $2, $3, $4, $5::jsonb, $6, $7, $8, $9)
80
+ returning id, user_id, slug, title, markdown, parsed, visibility, source_name,
81
+ created_at, updated_at`, [
82
+ userId,
83
+ slug,
84
+ title,
85
+ markdown,
86
+ JSON.stringify(parsed),
87
+ input.visibility ?? 'private',
88
+ input.source?.name ?? null,
89
+ input.source?.mime ?? null,
90
+ input.source?.bytes ?? null,
91
+ ]);
92
+ const row = result.rows[0];
93
+ if (row === undefined)
94
+ throw new Error('resume insert returned no row');
95
+ return toResume(row);
96
+ }
97
+ export async function updateResume(pool, userId, slug, input) {
98
+ const markdown = clean(input.markdown, 200_000);
99
+ const parsed = parseResume(markdown);
100
+ const result = await pool.query(`update resumes
101
+ set markdown = $3,
102
+ parsed = $4::jsonb,
103
+ title = coalesce(nullif($5, ''), title),
104
+ visibility = coalesce($6, visibility)
105
+ where user_id = $1 and slug = $2
106
+ returning id, user_id, slug, title, markdown, parsed, visibility, source_name,
107
+ created_at, updated_at`, [userId, slug, markdown, JSON.stringify(parsed), clean(input.title, 120), input.visibility ?? null]);
108
+ const row = result.rows[0];
109
+ return row === undefined ? null : toResume(row);
110
+ }
111
+ export async function deleteResume(pool, userId, slug) {
112
+ const result = await pool.query(`delete from resumes where user_id = $1 and slug = $2`, [
113
+ userId,
114
+ slug,
115
+ ]);
116
+ return (result.rowCount ?? 0) > 0;
117
+ }
118
+ /** The original upload, for re-converting a document that came out badly. */
119
+ export async function getResumeSource(pool, userId, slug) {
120
+ const result = await pool.query(`select source_name, source_mime, source_bytes from resumes where user_id = $1 and slug = $2`, [
121
+ userId,
122
+ slug,
123
+ ]);
124
+ const row = result.rows[0];
125
+ if (row === undefined || row.source_bytes === null)
126
+ return null;
127
+ return {
128
+ name: row.source_name ?? 'resume',
129
+ mime: row.source_mime ?? 'application/octet-stream',
130
+ bytes: row.source_bytes,
131
+ };
132
+ }
133
+ /** A first resume, so the editor is never an empty box. */
134
+ export async function ensureFirstResume(pool, userId, name) {
135
+ const existing = await listResumes(pool, userId);
136
+ const first = existing[0];
137
+ if (first !== undefined)
138
+ return first;
139
+ return createResume(pool, userId, {
140
+ title: 'Resume',
141
+ markdown: resumeTemplate(name ?? 'Your Name'),
142
+ });
143
+ }
144
+ //# sourceMappingURL=resumes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resumes.js","sourceRoot":"","sources":["../../src/core/resumes.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAGH,OAAO,EAAE,WAAW,EAAE,cAAc,EAAmB,MAAM,qBAAqB,CAAC;AACnF,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAC;AAE3D,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,SAAS,EAAE,MAAM,EAAE,QAAQ,CAAU,CAAC;AAGnE,MAAM,UAAU,YAAY,CAAC,KAAc;IACzC,OAAO,OAAO,KAAK,KAAK,QAAQ,IAAK,YAAkC,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC;AAC1F,CAAC;AA4BD,SAAS,QAAQ,CAAC,GAAc;IAC9B,OAAO;QACL,EAAE,EAAE,GAAG,CAAC,EAAE;QACV,MAAM,EAAE,GAAG,CAAC,OAAO;QACnB,IAAI,EAAE,GAAG,CAAC,IAAI;QACd,KAAK,EAAE,GAAG,CAAC,KAAK;QAChB,QAAQ,EAAE,GAAG,CAAC,QAAQ;QACtB,MAAM,EAAE,GAAG,CAAC,MAAM;QAClB,UAAU,EAAE,YAAY,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,UAAU,CAAC,CAAC,CAAC,SAAS;QACrE,UAAU,EAAE,GAAG,CAAC,WAAW;QAC3B,SAAS,EAAE,GAAG,CAAC,UAAU;QACzB,SAAS,EAAE,GAAG,CAAC,UAAU;KAC1B,CAAC;AACJ,CAAC;AAED,8EAA8E;AAC9E,6EAA6E;AAC7E,sBAAsB;AACtB,MAAM,MAAM,GAAG;2DAC4C,CAAC;AAE5D,MAAM,CAAC,KAAK,UAAU,WAAW,CAAC,IAAa,EAAE,MAAc;IAC7D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B,GAAG,MAAM,8CAA8C,EACvD,CAAC,MAAM,CAAC,CACT,CAAC;IACF,OAAO,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;AACnC,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,IAAa,EACb,MAAc,EACd,IAAY;IAEZ,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAY,GAAG,MAAM,mCAAmC,EAAE;QACvF,MAAM;QACN,IAAI;KACL,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,IAAa,EAAE,EAAU;IAC3D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAY,GAAG,MAAM,gBAAgB,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC5E,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAClD,CAAC;AAED,+DAA+D;AAC/D,MAAM,CAAC,KAAK,UAAU,eAAe,CAAC,IAAa,EAAE,EAAU;IAC7D,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B,GAAG,MAAM,qDAAqD,EAC9D,CAAC,EAAE,CAAC,CACL,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAClD,CAAC;AASD,KAAK,UAAU,UAAU,CAAC,IAAa,EAAE,MAAc,EAAE,KAAa;IACpE,MAAM,IAAI,GAAG,OAAO,CAAC,KAAK,CAAC,CAAC;IAC5B,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,CAAC,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAChD,MAAM,SAAS,GAAG,OAAO,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,GAAG,IAAI,IAAI,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;QAChE,MAAM,KAAK,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,wDAAwD,EAAE;YACvF,MAAM;YACN,SAAS;SACV,CAAC,CAAC;QACH,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;IAChD,CAAC;IACD,OAAO,GAAG,IAAI,IAAI,IAAI,CAAC,GAAG,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,CAAC;AAC9C,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAa,EACb,MAAc,EACd,KAAiB;IAEjB,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACrC,4EAA4E;IAC5E,qCAAqC;IACrC,MAAM,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,IAAI,MAAM,CAAC,IAAI,IAAI,QAAQ,CAAC;IACjE,MAAM,IAAI,GAAG,MAAM,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,CAAC,CAAC;IAEnD,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B;;;;sCAIkC,EAClC;QACE,MAAM;QACN,IAAI;QACJ,KAAK;QACL,QAAQ;QACR,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC;QACtB,KAAK,CAAC,UAAU,IAAI,SAAS;QAC7B,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI;QAC1B,KAAK,CAAC,MAAM,EAAE,IAAI,IAAI,IAAI;QAC1B,KAAK,CAAC,MAAM,EAAE,KAAK,IAAI,IAAI;KAC5B,CACF,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,GAAG,KAAK,SAAS;QAAE,MAAM,IAAI,KAAK,CAAC,+BAA+B,CAAC,CAAC;IACxE,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC;AACvB,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAChC,IAAa,EACb,MAAc,EACd,IAAY,EACZ,KAAiB;IAEjB,MAAM,QAAQ,GAAG,KAAK,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,WAAW,CAAC,QAAQ,CAAC,CAAC;IACrC,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAC7B;;;;;;;uCAOmC,EACnC,CAAC,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,CAAC,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,EAAE,GAAG,CAAC,EAAE,KAAK,CAAC,UAAU,IAAI,IAAI,CAAC,CACpG,CAAC;IACF,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,IAAa,EAAE,MAAc,EAAE,IAAY;IAC5E,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,sDAAsD,EAAE;QACtF,MAAM;QACN,IAAI;KACL,CAAC,CAAC;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC;AACpC,CAAC;AAED,6EAA6E;AAC7E,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,IAAa,EACb,MAAc,EACd,IAAY;IAEZ,MAAM,MAAM,GAAG,MAAM,IAAI,CAAC,KAAK,CAI5B,6FAA6F,EAAE;QAChG,MAAM;QACN,IAAI;KACL,CAAC,CAAC;IACH,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;IAC3B,IAAI,GAAG,KAAK,SAAS,IAAI,GAAG,CAAC,YAAY,KAAK,IAAI;QAAE,OAAO,IAAI,CAAC;IAChE,OAAO;QACL,IAAI,EAAE,GAAG,CAAC,WAAW,IAAI,QAAQ;QACjC,IAAI,EAAE,GAAG,CAAC,WAAW,IAAI,0BAA0B;QACnD,KAAK,EAAE,GAAG,CAAC,YAAY;KACxB,CAAC;AACJ,CAAC;AAED,2DAA2D;AAC3D,MAAM,CAAC,KAAK,UAAU,iBAAiB,CACrC,IAAa,EACb,MAAc,EACd,IAAmB;IAEnB,MAAM,QAAQ,GAAG,MAAM,WAAW,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;IACjD,MAAM,KAAK,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IAC1B,IAAI,KAAK,KAAK,SAAS;QAAE,OAAO,KAAK,CAAC;IACtC,OAAO,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE;QAChC,KAAK,EAAE,QAAQ;QACf,QAAQ,EAAE,cAAc,CAAC,IAAI,IAAI,WAAW,CAAC;KAC9C,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Just enough ZIP to read one file out of a .docx.
3
+ *
4
+ * A .docx is a ZIP holding word/document.xml. Pulling that one entry out needs
5
+ * the central directory and one inflate, which Node can already do, so this
6
+ * saves a dependency on a general-purpose archive library for a job with
7
+ * exactly one shape.
8
+ *
9
+ * The central directory is read rather than the local headers, because a
10
+ * local header is allowed to carry zero for the sizes and defer them to a data
11
+ * descriptor after the compressed bytes - which is exactly what several Word
12
+ * versions emit, and what makes the naive "scan for the filename" approach
13
+ * return an empty document instead of an error.
14
+ */
15
+ export declare class ZipProblem extends Error {
16
+ }
17
+ export declare function readZipEntry(buffer: Buffer, wanted: string): Buffer | null;
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Just enough ZIP to read one file out of a .docx.
3
+ *
4
+ * A .docx is a ZIP holding word/document.xml. Pulling that one entry out needs
5
+ * the central directory and one inflate, which Node can already do, so this
6
+ * saves a dependency on a general-purpose archive library for a job with
7
+ * exactly one shape.
8
+ *
9
+ * The central directory is read rather than the local headers, because a
10
+ * local header is allowed to carry zero for the sizes and defer them to a data
11
+ * descriptor after the compressed bytes - which is exactly what several Word
12
+ * versions emit, and what makes the naive "scan for the filename" approach
13
+ * return an empty document instead of an error.
14
+ */
15
+ import { inflateRawSync } from 'node:zlib';
16
+ const EOCD_SIGNATURE = 0x06054b50;
17
+ const CENTRAL_SIGNATURE = 0x02014b50;
18
+ export class ZipProblem extends Error {
19
+ }
20
+ function findEndOfCentralDirectory(buffer) {
21
+ // The comment field means the record is not necessarily at the very end.
22
+ const earliest = Math.max(0, buffer.length - 0xffff - 22);
23
+ for (let offset = buffer.length - 22; offset >= earliest; offset -= 1) {
24
+ if (buffer.readUInt32LE(offset) === EOCD_SIGNATURE)
25
+ return offset;
26
+ }
27
+ throw new ZipProblem('This does not look like a .docx (no ZIP directory found).');
28
+ }
29
+ function readEntries(buffer) {
30
+ const eocd = findEndOfCentralDirectory(buffer);
31
+ const count = buffer.readUInt16LE(eocd + 10);
32
+ let offset = buffer.readUInt32LE(eocd + 16);
33
+ const entries = [];
34
+ for (let index = 0; index < count; index += 1) {
35
+ if (offset + 46 > buffer.length)
36
+ break;
37
+ if (buffer.readUInt32LE(offset) !== CENTRAL_SIGNATURE)
38
+ break;
39
+ const method = buffer.readUInt16LE(offset + 10);
40
+ const compressedSize = buffer.readUInt32LE(offset + 20);
41
+ const nameLength = buffer.readUInt16LE(offset + 28);
42
+ const extraLength = buffer.readUInt16LE(offset + 30);
43
+ const commentLength = buffer.readUInt16LE(offset + 32);
44
+ const localHeaderOffset = buffer.readUInt32LE(offset + 42);
45
+ const name = buffer.toString('utf8', offset + 46, offset + 46 + nameLength);
46
+ entries.push({ name, method, compressedSize, localHeaderOffset });
47
+ offset += 46 + nameLength + extraLength + commentLength;
48
+ }
49
+ return entries;
50
+ }
51
+ export function readZipEntry(buffer, wanted) {
52
+ const entry = readEntries(buffer).find((item) => item.name === wanted);
53
+ if (entry === undefined)
54
+ return null;
55
+ const local = entry.localHeaderOffset;
56
+ if (local + 30 > buffer.length)
57
+ throw new ZipProblem('Truncated archive.');
58
+ // The local header's own name and extra lengths are the authoritative ones:
59
+ // the extra field is routinely a different length here than in the central
60
+ // directory, and using the wrong one lands mid-stream.
61
+ const nameLength = buffer.readUInt16LE(local + 26);
62
+ const extraLength = buffer.readUInt16LE(local + 28);
63
+ const start = local + 30 + nameLength + extraLength;
64
+ const data = buffer.subarray(start, start + entry.compressedSize);
65
+ if (entry.method === 0)
66
+ return Buffer.from(data);
67
+ if (entry.method === 8) {
68
+ try {
69
+ return inflateRawSync(data);
70
+ }
71
+ catch (error) {
72
+ throw new ZipProblem(`Could not decompress ${wanted}: ${error instanceof Error ? error.message : String(error)}`);
73
+ }
74
+ }
75
+ throw new ZipProblem(`Unsupported compression method ${entry.method} for ${wanted}.`);
76
+ }
77
+ //# sourceMappingURL=zip.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"zip.js","sourceRoot":"","sources":["../../src/core/zip.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { getPool, closePool } from './pool.ts';
2
+ export { migrate, migrationsDir } from './migrate.ts';
3
+ export type { MigrationResult } from './migrate.ts';
@@ -0,0 +1,3 @@
1
+ export { getPool, closePool } from "./pool.js";
2
+ export { migrate, migrationsDir } from "./migrate.js";
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/db/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAC/C,OAAO,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Forward-only migrations, applied at boot.
3
+ *
4
+ * Every `.sql` file in `migrations/` runs once, in filename order, inside its
5
+ * own transaction, and is recorded by name. Files are never edited after they
6
+ * have shipped: a change to an applied file is invisible to every database
7
+ * that already ran it.
8
+ */
9
+ import type pg from 'pg';
10
+ export declare function migrationsDir(): string;
11
+ export interface MigrationResult {
12
+ applied: string[];
13
+ alreadyApplied: number;
14
+ }
15
+ export declare function migrate(pool: pg.Pool, dir?: string): Promise<MigrationResult>;
@@ -0,0 +1,67 @@
1
+ /**
2
+ * Forward-only migrations, applied at boot.
3
+ *
4
+ * Every `.sql` file in `migrations/` runs once, in filename order, inside its
5
+ * own transaction, and is recorded by name. Files are never edited after they
6
+ * have shipped: a change to an applied file is invisible to every database
7
+ * that already ran it.
8
+ */
9
+ import { readdir, readFile } from 'node:fs/promises';
10
+ import { dirname, join } from 'node:path';
11
+ import { fileURLToPath } from 'node:url';
12
+ /**
13
+ * A boot-time migration and more than one process starting at once is a race
14
+ * every deployment of this shape eventually hits: the web process and the
15
+ * worker come up together, both see an unapplied file, and the loser fails on
16
+ * a duplicate object. A session-level advisory lock serialises them; the
17
+ * number is arbitrary but must never change.
18
+ */
19
+ const LOCK_ID = 8_274_119;
20
+ export function migrationsDir() {
21
+ // dist/db/migrate.js -> package root -> migrations
22
+ return join(dirname(dirname(dirname(fileURLToPath(import.meta.url)))), 'migrations');
23
+ }
24
+ export async function migrate(pool, dir = migrationsDir()) {
25
+ const files = (await readdir(dir))
26
+ .filter((name) => name.endsWith('.sql'))
27
+ .sort((a, b) => a.localeCompare(b, 'en'));
28
+ const client = await pool.connect();
29
+ const applied = [];
30
+ try {
31
+ await client.query('select pg_advisory_lock($1)', [LOCK_ID]);
32
+ await client.query(`
33
+ create table if not exists schema_migrations (
34
+ name text primary key,
35
+ applied_at timestamptz not null default now()
36
+ )
37
+ `);
38
+ const done = await client.query('select name from schema_migrations');
39
+ const seen = new Set(done.rows.map((row) => row.name));
40
+ for (const name of files) {
41
+ if (seen.has(name))
42
+ continue;
43
+ const sql = await readFile(join(dir, name), 'utf8');
44
+ try {
45
+ await client.query('begin');
46
+ // Sent whole, never split on semicolons: a plpgsql function body
47
+ // contains its own statements and splitting cuts one in half.
48
+ await client.query(sql);
49
+ await client.query('insert into schema_migrations (name) values ($1)', [name]);
50
+ await client.query('commit');
51
+ }
52
+ catch (error) {
53
+ await client.query('rollback');
54
+ throw new Error(`migration ${name} failed: ${error instanceof Error ? error.message : String(error)}`, { cause: error });
55
+ }
56
+ applied.push(name);
57
+ }
58
+ return { applied, alreadyApplied: seen.size };
59
+ }
60
+ finally {
61
+ // Released explicitly rather than by disconnecting, because the client is
62
+ // going back into the pool with the session-level lock still held.
63
+ await client.query('select pg_advisory_unlock($1)', [LOCK_ID]).catch(() => undefined);
64
+ client.release();
65
+ }
66
+ }
67
+ //# sourceMappingURL=migrate.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"migrate.js","sourceRoot":"","sources":["../../src/db/migrate.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,13 @@
1
+ /**
2
+ * One Postgres pool for the process.
3
+ *
4
+ * `pg` is CommonJS, so the named exports have to come off the default import;
5
+ * destructuring them in the import statement fails under NodeNext with a
6
+ * message about the module having no such export, which is misleading.
7
+ */
8
+ import pg from 'pg';
9
+ export type { PoolClient, QueryResult } from 'pg';
10
+ export type Pool = pg.Pool;
11
+ export declare function getPool(databaseUrl: string): pg.Pool;
12
+ /** For tests and for `agenticjobs` subcommands that exit when they are done. */
13
+ export declare function closePool(): Promise<void>;
@@ -0,0 +1,68 @@
1
+ /**
2
+ * One Postgres pool for the process.
3
+ *
4
+ * `pg` is CommonJS, so the named exports have to come off the default import;
5
+ * destructuring them in the import statement fails under NodeNext with a
6
+ * message about the module having no such export, which is misleading.
7
+ */
8
+ import pg from 'pg';
9
+ const { Pool, types } = pg;
10
+ // `timestamptz` (OID 1184) and `timestamp` (1114) arrive as JS Dates by
11
+ // default, which then serialise differently depending on which surface does
12
+ // the serialising. Everything in this codebase treats a time as an ISO string,
13
+ // so the conversion happens once, here.
14
+ types.setTypeParser(1184, (value) => new Date(value).toISOString());
15
+ types.setTypeParser(1114, (value) => new Date(`${value}Z`).toISOString());
16
+ // `int8` comes back as a string so that large values survive. Every count in
17
+ // this schema is comfortably inside Number.MAX_SAFE_INTEGER.
18
+ types.setTypeParser(20, (value) => Number.parseInt(value, 10));
19
+ let pool = null;
20
+ export function getPool(databaseUrl) {
21
+ if (pool !== null)
22
+ return pool;
23
+ pool = new Pool({
24
+ connectionString: databaseUrl,
25
+ max: Number.parseInt(process.env['PG_POOL_MAX'] ?? '10', 10) || 10,
26
+ idleTimeoutMillis: 30_000,
27
+ connectionTimeoutMillis: 10_000,
28
+ // Managed Postgres almost always presents a certificate this process has
29
+ // no root for. Refusing to connect would be correct and would also mean
30
+ // nobody can deploy, so verification is opt-in through PGSSLMODE.
31
+ ...(needsSsl(databaseUrl) ? { ssl: { rejectUnauthorized: false } } : {}),
32
+ });
33
+ // A pool that emits an error with no listener takes the process down, and
34
+ // the common cause is a database restart the app could simply reconnect
35
+ // through.
36
+ pool.on('error', (error) => {
37
+ console.error('postgres pool error', error.message);
38
+ });
39
+ return pool;
40
+ }
41
+ function needsSsl(databaseUrl) {
42
+ const mode = process.env['PGSSLMODE']?.trim().toLowerCase();
43
+ if (mode === 'disable')
44
+ return false;
45
+ if (mode !== undefined && mode !== '')
46
+ return true;
47
+ try {
48
+ const url = new URL(databaseUrl);
49
+ if (url.searchParams.get('sslmode') === 'disable')
50
+ return false;
51
+ if (url.searchParams.has('sslmode'))
52
+ return true;
53
+ // A local database is the only one where plaintext is the sane default.
54
+ return !['localhost', '127.0.0.1', '::1', 'postgres', 'db'].includes(url.hostname);
55
+ }
56
+ catch {
57
+ return false;
58
+ }
59
+ }
60
+ /** For tests and for `agenticjobs` subcommands that exit when they are done. */
61
+ export async function closePool() {
62
+ if (pool === null)
63
+ return;
64
+ const closing = pool;
65
+ pool = null;
66
+ await closing.end();
67
+ }
68
+ //# sourceMappingURL=pool.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"pool.js","sourceRoot":"","sources":["../../src/db/pool.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Example data, so a fresh board is not an empty page.
3
+ *
4
+ * Refuses to run on a board that already has jobs. Seed data appearing on a
5
+ * live board next to real listings would be worse than an empty one, and this
6
+ * is a command people run twice by accident.
7
+ */
8
+ import type pg from 'pg';
9
+ export declare function seed(pool: pg.Pool): Promise<number>;
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Example data, so a fresh board is not an empty page.
3
+ *
4
+ * Refuses to run on a board that already has jobs. Seed data appearing on a
5
+ * live board next to real listings would be worse than an empty one, and this
6
+ * is a command people run twice by accident.
7
+ */
8
+ import { createJob, countJobs, normaliseInput, setStatus } from "../core/jobs.js";
9
+ import { createOrg } from "../core/orgs.js";
10
+ import { ensureUser } from "../core/auth.js";
11
+ const EXAMPLES = [
12
+ {
13
+ title: 'Staff Engineer, Agent Platform',
14
+ description: `We are building the plumbing that lets other people's agents do real work: sandboxes, tool registries, audit trails.
15
+
16
+ ## What you would own
17
+
18
+ The execution layer. When someone's agent calls a tool, everything between the call and the result is yours: isolation, quotas, retries, and the record of what happened.
19
+
20
+ ## What we are looking for
21
+
22
+ Someone who has run something in production that other people depended on, and who has opinions about failure modes that came from watching them happen.
23
+
24
+ You will be reviewing a lot of machine-written code. We would rather hire someone who is good at that than someone who is offended by it.`,
25
+ workplace: 'remote',
26
+ seniority: 'staff',
27
+ location: 'European timezones',
28
+ salaryMin: 180000,
29
+ salaryMax: 230000,
30
+ stack: 'typescript, postgres, kubernetes, rust',
31
+ tags: 'infrastructure, agents, backend',
32
+ agentPolicy: 'welcome',
33
+ },
34
+ {
35
+ title: 'Founding Designer',
36
+ description: `Three engineers and no designer, which is exactly as visible in the product as it sounds.
37
+
38
+ ## The job
39
+
40
+ Own what it looks like and how it works, end to end. There is no design system yet; you would be making it, then living with it.
41
+
42
+ ## Honestly
43
+
44
+ Early, uncertain, and the equity is a bet not a salary. Say no if that is not what you want right now.`,
45
+ workplace: 'hybrid',
46
+ seniority: 'senior',
47
+ location: 'Berlin',
48
+ salaryMin: 85000,
49
+ salaryMax: 110000,
50
+ stack: 'figma, css, typescript',
51
+ tags: 'design, frontend',
52
+ agentPolicy: 'human-only',
53
+ },
54
+ {
55
+ title: 'Backend Engineer, Payments',
56
+ description: `Money moving correctly, every time, with a paper trail.
57
+
58
+ ## What you would do
59
+
60
+ - Ledger work: double-entry, reconciliation, the boring parts that must be right
61
+ - Integrations with banking partners whose APIs were designed in 2009
62
+ - On-call for a system where an outage is measured in money
63
+
64
+ ## What we care about
65
+
66
+ Correctness over speed, and the judgment to know when that is the wrong trade.`,
67
+ workplace: 'remote',
68
+ seniority: 'mid',
69
+ location: 'Anywhere',
70
+ salaryMin: 120000,
71
+ salaryMax: 160000,
72
+ stack: 'go, postgres, kafka',
73
+ tags: 'backend, fintech, payments',
74
+ agentPolicy: 'disclose',
75
+ },
76
+ {
77
+ title: 'Developer Advocate',
78
+ description: `Write the documentation you wish you had found, then go and find out why people still could not use it.
79
+
80
+ ## The work
81
+
82
+ Half writing, half talking to people who are stuck. Conference talks if you like them; nobody will make you.
83
+
84
+ ## Not the work
85
+
86
+ This is not a marketing role with a technical coat of paint. You would be shipping code that goes in the docs.`,
87
+ workplace: 'remote',
88
+ seniority: 'mid',
89
+ location: 'US timezones',
90
+ salaryMin: 110000,
91
+ salaryMax: 140000,
92
+ stack: 'typescript, python, markdown',
93
+ tags: 'devrel, docs',
94
+ agentPolicy: 'welcome',
95
+ },
96
+ ];
97
+ export async function seed(pool) {
98
+ const counts = await countJobs(pool);
99
+ if (counts.total > 0)
100
+ return 0;
101
+ const user = await ensureUser(pool, 'example@example.com', 'Example');
102
+ const org = await createOrg(pool, user.id, {
103
+ name: 'Example Works',
104
+ website: 'https://example.com',
105
+ description: 'A placeholder employer, created by `agenticjobs seed`. Delete it before anyone real turns up.',
106
+ });
107
+ if (typeof org === 'string')
108
+ throw new Error(org);
109
+ let added = 0;
110
+ for (const example of EXAMPLES) {
111
+ const input = normaliseInput({ ...example }, org.id);
112
+ if (typeof input === 'string')
113
+ throw new Error(`seed rejected: ${input}`);
114
+ const job = await createJob(pool, input);
115
+ await setStatus(pool, job.id, 'published');
116
+ added += 1;
117
+ }
118
+ return added;
119
+ }
120
+ //# sourceMappingURL=seed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"seed.js","sourceRoot":"","sources":["../../src/db/seed.ts"],"names":[],"mappings":"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"}