@profullstack/agenticjobs 0.10.0 → 0.12.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 (108) hide show
  1. package/README.md +69 -1
  2. package/dist/cli/args.js +31 -3
  3. package/dist/cli/args.js.map +1 -1
  4. package/dist/cli/index.js +396 -3
  5. package/dist/cli/index.js.map +1 -1
  6. package/dist/cli/jobfile.js +38 -2
  7. package/dist/cli/jobfile.js.map +1 -1
  8. package/dist/client/client.d.ts +149 -0
  9. package/dist/client/client.js +79 -12
  10. package/dist/client/client.js.map +1 -1
  11. package/dist/client/fanout.js +7 -1
  12. package/dist/client/fanout.js.map +1 -1
  13. package/dist/config.d.ts +35 -1
  14. package/dist/config.js +33 -1
  15. package/dist/config.js.map +1 -1
  16. package/dist/core/candidates.js +33 -6
  17. package/dist/core/candidates.js.map +1 -1
  18. package/dist/core/capacity-alert.d.ts +84 -0
  19. package/dist/core/capacity-alert.js +144 -0
  20. package/dist/core/capacity-alert.js.map +1 -0
  21. package/dist/core/capacity.d.ts +96 -0
  22. package/dist/core/capacity.js +174 -0
  23. package/dist/core/capacity.js.map +1 -0
  24. package/dist/core/coinpay.d.ts +143 -0
  25. package/dist/core/coinpay.js +417 -0
  26. package/dist/core/coinpay.js.map +1 -0
  27. package/dist/core/import-job.js +18 -7
  28. package/dist/core/import-job.js.map +1 -1
  29. package/dist/core/import.js +18 -15
  30. package/dist/core/import.js.map +1 -1
  31. package/dist/core/inbox.d.ts +155 -0
  32. package/dist/core/inbox.js +353 -0
  33. package/dist/core/inbox.js.map +1 -0
  34. package/dist/core/invoices.d.ts +111 -0
  35. package/dist/core/invoices.js +292 -0
  36. package/dist/core/invoices.js.map +1 -0
  37. package/dist/core/jobs.js +12 -2
  38. package/dist/core/jobs.js.map +1 -1
  39. package/dist/core/mail.d.ts +16 -0
  40. package/dist/core/mail.js +22 -0
  41. package/dist/core/mail.js.map +1 -1
  42. package/dist/core/orgs.d.ts +39 -0
  43. package/dist/core/orgs.js +69 -0
  44. package/dist/core/orgs.js.map +1 -1
  45. package/dist/directory/federate.js +2 -4
  46. package/dist/directory/federate.js.map +1 -1
  47. package/dist/directory/fetch.d.ts +1 -1
  48. package/dist/markup/markdown.js +4 -1
  49. package/dist/markup/markdown.js.map +1 -1
  50. package/dist/markup/resume.js +66 -1
  51. package/dist/markup/resume.js.map +1 -1
  52. package/dist/mcp/server.js +5 -1
  53. package/dist/mcp/server.js.map +1 -1
  54. package/dist/mcp/stdio.js +3 -2
  55. package/dist/mcp/stdio.js.map +1 -1
  56. package/dist/mcp/tools.js +151 -1
  57. package/dist/mcp/tools.js.map +1 -1
  58. package/dist/schema/jsonld.js +1 -8
  59. package/dist/schema/jsonld.js.map +1 -1
  60. package/dist/schema/salary.d.ts +5 -0
  61. package/dist/schema/salary.js +15 -0
  62. package/dist/schema/salary.js.map +1 -0
  63. package/dist/schema/text.js +7 -3
  64. package/dist/schema/text.js.map +1 -1
  65. package/dist/server/app.d.ts +4 -3
  66. package/dist/server/app.js +7 -4
  67. package/dist/server/app.js.map +1 -1
  68. package/dist/server/deps.d.ts +5 -0
  69. package/dist/server/middleware.js +9 -1
  70. package/dist/server/middleware.js.map +1 -1
  71. package/dist/server/routes/api.js +347 -1
  72. package/dist/server/routes/api.js.map +1 -1
  73. package/dist/server/routes/discovery.js +75 -0
  74. package/dist/server/routes/discovery.js.map +1 -1
  75. package/dist/server/routes/inbox.d.ts +9 -0
  76. package/dist/server/routes/inbox.js +223 -0
  77. package/dist/server/routes/inbox.js.map +1 -0
  78. package/dist/server/routes/openapi.js +141 -1
  79. package/dist/server/routes/openapi.js.map +1 -1
  80. package/dist/server/routes/pages.d.ts +22 -0
  81. package/dist/server/routes/pages.js +91 -7
  82. package/dist/server/routes/pages.js.map +1 -1
  83. package/dist/views/candidates.d.ts +8 -0
  84. package/dist/views/candidates.js +2 -1
  85. package/dist/views/candidates.js.map +1 -1
  86. package/dist/views/docs.js +48 -4
  87. package/dist/views/docs.js.map +1 -1
  88. package/dist/views/inbox.d.ts +90 -0
  89. package/dist/views/inbox.js +73 -0
  90. package/dist/views/inbox.js.map +1 -0
  91. package/dist/views/jobs.d.ts +9 -0
  92. package/dist/views/jobs.js +3 -2
  93. package/dist/views/jobs.js.map +1 -1
  94. package/dist/views/layout.d.ts +2 -0
  95. package/dist/views/layout.js +2 -2
  96. package/dist/views/layout.js.map +1 -1
  97. package/dist/views/me.d.ts +3 -3
  98. package/dist/views/me.js +2 -2
  99. package/dist/views/me.js.map +1 -1
  100. package/dist/views/updates.d.ts +13 -0
  101. package/dist/views/updates.js +2 -1
  102. package/dist/views/updates.js.map +1 -1
  103. package/docs/openjob.md +5 -0
  104. package/docs/openresume.md +36 -0
  105. package/migrations/0014_inbox_and_billing.sql +138 -0
  106. package/package.json +15 -17
  107. package/web/public/app.css +146 -3
  108. package/web/public/sw.js +1 -1
package/README.md CHANGED
@@ -63,7 +63,9 @@ curl https://agenticjobs.work/.well-known/agenticjobs
63
63
  curl "https://agenticjobs.work/api/v1/jobs?q=rust&workplace=remote&agentPolicy=welcome"
64
64
 
65
65
  # the application form, as data
66
- curl https://agenticjobs.work/api/v1/jobs/SLUG/apply-schema
66
+ # First fetch after idle can exceed 15s; retry once rather than skipping.
67
+ curl --retry 1 --retry-all-errors --max-time 30 \
68
+ https://agenticjobs.work/api/v1/jobs/SLUG/apply-schema
67
69
 
68
70
  # send it
69
71
  curl -X POST https://agenticjobs.work/api/v1/jobs/SLUG/apply \
@@ -269,6 +271,72 @@ author, and the same text twice is refused. 600 characters and one link, rendere
269
271
  text and marked `nofollow`, because a board with an open posting form is a link farm
270
272
  the moment it passes PageRank on.
271
273
 
274
+ ## Inbox, and invoices
275
+
276
+ There is no public comment box anywhere on the board. A board with one gets used as
277
+ the place to hand in invoices, because an invoice has to go somewhere and a comment
278
+ under a listing was the only somewhere. So reaching somebody here is private: a
279
+ conversation between you and a candidate, or you and an employer, and nobody else.
280
+ Writing to an employer reaches every member of it.
281
+
282
+ ```bash
283
+ agenticjobs message acme "Is the Go role open to contractors?" --job senior-go-engineer
284
+ agenticjobs message jane-doe --candidate "Saw your resume. Free for a call this week?"
285
+ agenticjobs inbox # your conversations
286
+ agenticjobs inbox read <id> # one, with its invoices
287
+ agenticjobs reply <id> "Thursday works."
288
+ ```
289
+
290
+ The button is on every candidate's and every employer's page, and on a listing. Twenty
291
+ new conversations a day per account; replies are not counted. The other side is
292
+ emailed that there is a message, never the message itself: the conversation is private
293
+ to the people in it and somebody else's mail server is not.
294
+
295
+ An invoice is a message with money attached. The payee sends it from the conversation,
296
+ in US dollars, choosing a chain they hold a wallet for; the other side presses Pay and
297
+ settles it on [CoinPay](https://coinpayportal.com), straight to that wallet. The board
298
+ never holds the money.
299
+
300
+ ```bash
301
+ agenticjobs billing # is a CoinPay account connected?
302
+ agenticjobs invoice <thread-id> 1200 --currency USDC_POL --for "Sprint 3, as agreed"
303
+ agenticjobs invoices # sent and received
304
+ agenticjobs pay <invoice-id> # a quote, and the page to pay on
305
+ ```
306
+
307
+ Connecting a CoinPay account is a browser step (`/me`, Billing, Connect CoinPay): it is
308
+ CoinPay's consent screen, asking for `wallet:read` and nothing else. The board reads
309
+ which wallets you can be paid to and uses the token for nothing else; it cannot move
310
+ funds or create anything on your account.
311
+
312
+ ### Turning billing on
313
+
314
+ Billing is off until the operator gives the board its own CoinPay credentials. Two
315
+ sets, because two jobs: an OAuth client so *people* can connect their accounts, and a
316
+ business key so the *board* can mint the payment a payer settles. All four or none;
317
+ a partial set is logged at boot and leaves billing off rather than half on.
318
+
319
+ ```bash
320
+ COINPAY_CLIENT_ID=cp_... # coinpay oauth create --name your-board \
321
+ COINPAY_CLIENT_SECRET=cps_... # --redirect-uri https://your-board/api/v1/coinpay/callback \
322
+ # --scope openid,profile,email,wallet:read
323
+ COINPAY_API_KEY=cp_live_... # coinpay business create --name your-board \
324
+ COINPAY_BUSINESS_ID=... # --category marketplace \
325
+ COINPAY_WEBHOOK_SECRET=whsec_... # --webhook-url https://your-board/api/v1/coinpay/webhook
326
+ COINPAY_URL=https://coinpayportal.com # optional
327
+ ```
328
+
329
+ The redirect URI has to be registered on the OAuth client byte for byte, and the
330
+ client has to be registered for `wallet:read`: CoinPay narrows a grant to the client's
331
+ registered scopes without an error, and a board that trusts its own request shows
332
+ "Connected" beside an account it cannot read a wallet from. The board checks the scope
333
+ on the token it got back, and says "Reconnect required" when it is missing.
334
+
335
+ Payment is confirmed by CoinPay's webhook, and by asking CoinPay whenever the
336
+ conversation is opened, so a lost webhook delays the answer rather than losing it.
337
+ Without `COINPAY_WEBHOOK_SECRET` every webhook is refused and polling is the only
338
+ source, which works and is slower.
339
+
272
340
  ## Posting from myna
273
341
 
274
342
  A job opening goes out with the rest of a launch:
package/dist/cli/args.js CHANGED
@@ -5,6 +5,32 @@
5
5
  * Repeated flags collect into an array, because `--answer a=1 --answer b=2` is
6
6
  * how the apply command takes fields.
7
7
  */
8
+ // Boolean options must not consume the next command or search word. Keep
9
+ // explicit boolean values working for callers that pass `--remote false`.
10
+ const BOOLEAN_FLAGS = new Set([
11
+ 'version',
12
+ 'v',
13
+ 'help',
14
+ 'h',
15
+ 'yes',
16
+ 'y',
17
+ 'json',
18
+ 'remote',
19
+ 'agents',
20
+ 'all',
21
+ 'network',
22
+ 'unsupervised',
23
+ 'draft',
24
+ 'salary-unpaid',
25
+ 'publish',
26
+ 'following',
27
+ 'candidate',
28
+ ]);
29
+ function takesValue(name, next) {
30
+ return (next !== undefined &&
31
+ !next.startsWith('-') &&
32
+ (!BOOLEAN_FLAGS.has(name) || /^(true|false|1|0|yes|no)$/i.test(next)));
33
+ }
8
34
  export function parseArgs(argv) {
9
35
  const positional = [];
10
36
  const flags = {};
@@ -27,7 +53,7 @@ export function parseArgs(argv) {
27
53
  continue;
28
54
  }
29
55
  const next = argv[index + 1];
30
- if (next !== undefined && !next.startsWith('-')) {
56
+ if (takesValue(body, next)) {
31
57
  set(flags, body, next);
32
58
  index += 1;
33
59
  continue;
@@ -38,7 +64,7 @@ export function parseArgs(argv) {
38
64
  if (token.startsWith('-') && token.length > 1) {
39
65
  const body = token.slice(1);
40
66
  const next = argv[index + 1];
41
- if (next !== undefined && !next.startsWith('-')) {
67
+ if (takesValue(body, next)) {
42
68
  set(flags, body, next);
43
69
  index += 1;
44
70
  continue;
@@ -74,7 +100,9 @@ export function flagString(args, ...names) {
74
100
  }
75
101
  export function flagBool(args, ...names) {
76
102
  for (const name of names) {
77
- const value = args.flags[name];
103
+ const raw = args.flags[name];
104
+ // Repeated options use their last value, just like flagString.
105
+ const value = Array.isArray(raw) ? raw[raw.length - 1] : raw;
78
106
  if (value === true)
79
107
  return true;
80
108
  if (typeof value === 'string')
@@ -1 +1 @@
1
- {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"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"}
package/dist/cli/index.js CHANGED
@@ -20,6 +20,10 @@ import { ApiError, BoardClient, configuredBoards, currentBoard, DEFAULT_SERVER,
20
20
  import { VERSION } from "../config.js";
21
21
  import { ago, formatSalary } from "../schema/text.js";
22
22
  import { APPLICATION_DECISIONS, isApplicationDecision } from "../schema/job.js";
23
+ // The vocabulary, not the storage: core/resumes.ts is pure apart from a type
24
+ // import of pg, so the CLI can name the same three values the server does
25
+ // rather than keeping a second copy of them in step by hand.
26
+ import { VISIBILITIES } from "../core/resumes.js";
23
27
  const USAGE = `agenticjobs ${VERSION} - an agent-friendly job board you can self-host
24
28
 
25
29
  Account
@@ -51,8 +55,12 @@ const USAGE = `agenticjobs ${VERSION} - an agent-friendly job board you can self
51
55
  Resumes
52
56
  resume list
53
57
  resume show <slug>
54
- resume save <file> [--slug s] [--title t]
58
+ resume save <file> [--slug s] [--title t] [--visibility v]
55
59
  resume import <file> pdf, docx, txt or md, converted to Markdown
60
+ resume publish <slug> list it at /candidates
61
+ resume unpublish <slug> make it private again
62
+ resume visibility <slug> <private|link|public>
63
+ resume delete <slug> --yes
56
64
 
57
65
  Updates
58
66
  news the updates on this board
@@ -63,7 +71,26 @@ const USAGE = `agenticjobs ${VERSION} - an agent-friendly job board you can self
63
71
  follow <slug> follow an employer; --candidate for a person
64
72
  unfollow <slug> stop
65
73
 
74
+ Inbox
75
+ inbox your conversations
76
+ inbox read <id> one conversation, and its invoices
77
+ message <slug> <text> write to an employer; --candidate for a person
78
+ --job <slug> about a listing
79
+ --as <employer-slug> write as an employer you belong to
80
+ --subject <text>
81
+ reply <id> <text> reply in a conversation
82
+ invoice <id> <amount> bill the other side, in USD; --currency <chain>
83
+ --for <text> what it is for
84
+ invoices every invoice you sent or can pay
85
+ pay <invoice-id> get a quote and the CoinPay page to pay on
86
+ billing whether your CoinPay account is connected
87
+
66
88
  Hiring
89
+ employer list the employers you can post under
90
+ employer show <slug>
91
+ employer create <name> [--website u] [--description d] [--logo u]
92
+ employer update <slug> [--name n] [--website u] [--description d]
93
+ employer delete <slug> --yes
67
94
  post <file.md> post a job; stays a draft until you publish
68
95
  new <url> import a job from a URL, as a draft
69
96
  update <url> re-read that URL into the listing it created
@@ -152,6 +179,50 @@ async function run(args) {
152
179
  await closePool();
153
180
  return 0;
154
181
  }
182
+ /**
183
+ * Ask listed candidates to state their agent capacity.
184
+ *
185
+ * Prints the recipients and stops. `--send` is what actually mails them,
186
+ * and it is a separate word on purpose: this is the only command in the
187
+ * CLI that writes to other people, and the operator should have to say so
188
+ * after reading the list.
189
+ */
190
+ case 'ask-capacity': {
191
+ const { closePool, getPool } = await import("../db/pool.js");
192
+ const { loadConfig: serverConfig } = await import("../config.js");
193
+ const { createMailer } = await import("../core/mail.js");
194
+ const { runCapacityAlerts } = await import("../core/capacity-alert.js");
195
+ const config = serverConfig();
196
+ const send = flagBool(args, 'send');
197
+ const mailer = send ? createMailer(config) : null;
198
+ if (send && mailer === null) {
199
+ process.stderr.write('No RESEND_API_KEY, so nothing can be sent.\n');
200
+ await closePool();
201
+ return 1;
202
+ }
203
+ const results = await runCapacityAlerts({
204
+ pool: getPool(config.databaseUrl),
205
+ mailer,
206
+ boardName: config.boardName,
207
+ publicUrl: config.publicUrl,
208
+ send,
209
+ });
210
+ if (results.length === 0) {
211
+ process.stdout.write('Every listed candidate states their capacity.\n');
212
+ await closePool();
213
+ return 0;
214
+ }
215
+ for (const result of results) {
216
+ const status = result.sent === null ? 'would ask' : result.sent ? 'asked' : 'FAILED';
217
+ process.stdout.write(`${status}: ${result.target.name} <${result.target.email}>\n`);
218
+ }
219
+ const failed = results.filter((result) => result.sent === false).length;
220
+ process.stdout.write(send
221
+ ? `\n${results.length - failed} sent, ${failed} failed.\n`
222
+ : `\n${results.length} candidate(s) would be asked. Re-run with --send to mail them.\n`);
223
+ await closePool();
224
+ return failed === 0 ? 0 : 1;
225
+ }
155
226
  case 'tui': {
156
227
  const { startTui } = await import("../tui/index.js");
157
228
  await startTui(clientFor(args));
@@ -200,6 +271,9 @@ async function run(args) {
200
271
  return commandSubmit(args);
201
272
  case 'resume':
202
273
  return commandResume(args);
274
+ case 'employer':
275
+ case 'employers':
276
+ return commandEmployer(args);
203
277
  case 'post':
204
278
  return commandPost(args);
205
279
  case 'new':
@@ -218,6 +292,20 @@ async function run(args) {
218
292
  case 'follow':
219
293
  case 'unfollow':
220
294
  return commandFollow(args);
295
+ case 'inbox':
296
+ return commandInbox(args);
297
+ case 'message':
298
+ return commandMessage(args);
299
+ case 'reply':
300
+ return commandReply(args);
301
+ case 'invoice':
302
+ return commandInvoice(args);
303
+ case 'invoices':
304
+ return commandInvoices(args);
305
+ case 'pay':
306
+ return commandPay(args);
307
+ case 'billing':
308
+ return commandBilling(args);
221
309
  case 'announce':
222
310
  return commandAnnounce(args);
223
311
  case 'instances':
@@ -612,11 +700,17 @@ async function commandResume(args) {
612
700
  }
613
701
  const slug = flagString(args, 'slug');
614
702
  const title = flagString(args, 'title');
703
+ const visibility = visibilityFrom(args);
704
+ if (visibility instanceof Error) {
705
+ process.stderr.write(`${visibility.message}\n`);
706
+ return 1;
707
+ }
615
708
  const saved = await client.saveResume(await readFile(path, 'utf8'), {
616
709
  ...(slug === undefined ? {} : { slug }),
617
710
  ...(title === undefined ? {} : { title }),
711
+ ...(visibility === undefined ? {} : { visibility }),
618
712
  });
619
- return out(args, 'Saved.', saved);
713
+ return out(args, savedMessage(client.server, saved), saved);
620
714
  }
621
715
  if (action === 'import') {
622
716
  const path = args.positional[1];
@@ -627,17 +721,195 @@ async function commandResume(args) {
627
721
  const { importDocument } = await import("../core/import.js");
628
722
  const imported = await importDocument(path, await readFile(path));
629
723
  const title = flagString(args, 'title');
724
+ const visibility = visibilityFrom(args);
725
+ if (visibility instanceof Error) {
726
+ process.stderr.write(`${visibility.message}\n`);
727
+ return 1;
728
+ }
630
729
  // Converted on this machine, so the original file never leaves it.
631
730
  const saved = await client.saveResume(imported.markdown, {
632
731
  ...(title === undefined ? {} : { title }),
732
+ ...(visibility === undefined ? {} : { visibility }),
633
733
  });
634
734
  for (const warning of imported.warnings)
635
735
  process.stderr.write(`note: ${warning}\n`);
636
- return out(args, `Converted from ${imported.via} and saved.`, saved);
736
+ return out(args, `Converted from ${imported.via}. ${savedMessage(client.server, saved)}`, saved);
737
+ }
738
+ /**
739
+ * Publishing is its own word because it is its own decision.
740
+ *
741
+ * Saving a resume and letting the board list it are different acts, and the
742
+ * flag on `save` is there for the person who means both at once. This is
743
+ * for the far more common case of changing your mind about a document that
744
+ * is already written.
745
+ */
746
+ if (action === 'publish' || action === 'unpublish' || action === 'visibility') {
747
+ const slug = args.positional[1];
748
+ if (slug === undefined) {
749
+ process.stderr.write(`Which one? agenticjobs resume ${action} <slug>\n`);
750
+ return 1;
751
+ }
752
+ let wanted;
753
+ if (action === 'publish')
754
+ wanted = 'public';
755
+ else if (action === 'unpublish')
756
+ wanted = 'private';
757
+ else {
758
+ const given = args.positional[2];
759
+ if (given === undefined || !VISIBILITIES.includes(given)) {
760
+ process.stderr.write(`Which visibility? agenticjobs resume visibility <slug> <${VISIBILITIES.join('|')}>\n`);
761
+ return 1;
762
+ }
763
+ wanted = given;
764
+ }
765
+ const result = await client.setResumeVisibility(slug, wanted);
766
+ return out(args, savedMessage(client.server, result), result);
767
+ }
768
+ if (action === 'delete') {
769
+ const slug = args.positional[1];
770
+ if (slug === undefined) {
771
+ process.stderr.write('Which one? agenticjobs resume delete <slug>\n');
772
+ return 1;
773
+ }
774
+ if (!flagBool(args, 'yes', 'y')) {
775
+ process.stderr.write(`This deletes ${slug} and cannot be undone. Add --yes if you mean it.\n`);
776
+ return 1;
777
+ }
778
+ const removed = await client.deleteResume(slug);
779
+ return out(args, `Deleted ${slug}.`, removed);
637
780
  }
638
781
  process.stderr.write(`No resume command called "${action}".\n`);
639
782
  return 1;
640
783
  }
784
+ // --- employers ------------------------------------------------------------
785
+ /**
786
+ * The employer you post under, from the terminal.
787
+ *
788
+ * Posting a job needed an employer and there was no way to make one without a
789
+ * browser, so the documented path for a board whose whole pitch is that an
790
+ * agent can use it was "open the website first". This is the missing half.
791
+ */
792
+ async function commandEmployer(args) {
793
+ const action = args.positional[0] ?? 'list';
794
+ const client = clientFor(args);
795
+ if (action === 'list') {
796
+ const orgs = await client.myOrgs();
797
+ if (orgs.length === 0) {
798
+ return out(args, `No employers yet. ${dim('agenticjobs employer create "Example Works"')}`, { items: orgs });
799
+ }
800
+ return out(args, orgs.map(employerLine).join('\n'), { items: orgs });
801
+ }
802
+ if (action === 'show') {
803
+ const slug = args.positional[1];
804
+ if (slug === undefined) {
805
+ process.stderr.write('Which one? agenticjobs employer show <slug>\n');
806
+ return 1;
807
+ }
808
+ const result = await client.request('GET', `/api/v1/orgs/${encodeURIComponent(slug)}`);
809
+ return out(args, [
810
+ employerLine(result.org),
811
+ result.org.description === null ? '' : ` ${result.org.description}`,
812
+ ` ${dim(`${result.jobs.total} published`)}`,
813
+ ]
814
+ .filter((line) => line !== '')
815
+ .join('\n'), result);
816
+ }
817
+ if (action === 'create') {
818
+ const name = args.positional.slice(1).join(' ').trim() || (flagString(args, 'name') ?? '');
819
+ if (name === '') {
820
+ process.stderr.write('What is it called? agenticjobs employer create "Example Works"\n');
821
+ return 1;
822
+ }
823
+ const created = await client.createOrg({ name, ...employerFields(args) });
824
+ return out(args, `Created ${created.org.slug}.\n${dim(` post to it: agenticjobs post job.md --org ${created.org.slug}`)}`, created);
825
+ }
826
+ if (action === 'update') {
827
+ const slug = args.positional[1];
828
+ if (slug === undefined) {
829
+ process.stderr.write('Which one? agenticjobs employer update <slug> --name "New Name"\n');
830
+ return 1;
831
+ }
832
+ const name = flagString(args, 'name');
833
+ const fields = { ...(name === undefined ? {} : { name }), ...employerFields(args) };
834
+ if (Object.keys(fields).length === 0) {
835
+ process.stderr.write('Nothing to change. Pass --name, --website, --description or --logo.\n');
836
+ return 1;
837
+ }
838
+ const updated = await client.updateOrg(slug, fields);
839
+ // The slug is deliberately not derived again from a new name, so say so:
840
+ // somebody who renames an employer will otherwise go looking for a URL
841
+ // that never changed.
842
+ return out(args, `Updated ${updated.org.name}. ${dim(`Still ${updated.org.slug}, so every link to it still works.`)}`, updated);
843
+ }
844
+ if (action === 'delete') {
845
+ const slug = args.positional[1];
846
+ if (slug === undefined) {
847
+ process.stderr.write('Which one? agenticjobs employer delete <slug> --yes\n');
848
+ return 1;
849
+ }
850
+ if (!flagBool(args, 'yes', 'y')) {
851
+ process.stderr.write(`This deletes ${slug} and cannot be undone. Add --yes if you mean it.\n`);
852
+ return 1;
853
+ }
854
+ const removed = await client.deleteOrg(slug);
855
+ return out(args, `Deleted ${slug}.`, removed);
856
+ }
857
+ process.stderr.write(`No employer command called "${action}".\n`);
858
+ return 1;
859
+ }
860
+ /**
861
+ * The optional details, only when they were given.
862
+ *
863
+ * An absent flag has to stay absent all the way to the request: the API
864
+ * leaves out what it is not sent, and sending `undefined` as `null` here
865
+ * would turn "I did not mention the website" into "clear the website".
866
+ */
867
+ function employerFields(args) {
868
+ const website = flagString(args, 'website');
869
+ const description = flagString(args, 'description');
870
+ const logo = flagString(args, 'logo', 'logo-url');
871
+ return {
872
+ ...(website === undefined ? {} : { website }),
873
+ ...(description === undefined ? {} : { description }),
874
+ ...(logo === undefined ? {} : { logoUrl: logo }),
875
+ };
876
+ }
877
+ function employerLine(org) {
878
+ const bits = [org.slug, org.website].filter((bit) => typeof bit === 'string' && bit !== '');
879
+ return `${org.name} ${dim(bits.join(' '))}`;
880
+ }
881
+ /** `--visibility public`, checked here so a typo is not silently ignored. */
882
+ function visibilityFrom(args) {
883
+ const given = flagString(args, 'visibility');
884
+ if (given === undefined)
885
+ return undefined;
886
+ if (!VISIBILITIES.includes(given)) {
887
+ return new Error(`Not a visibility: ${given}. Use ${VISIBILITIES.join(', ')}.`);
888
+ }
889
+ return given;
890
+ }
891
+ /**
892
+ * What happened, said in terms of who can now see it.
893
+ *
894
+ * "Saved." was true and useless: the whole question a candidate has after
895
+ * saving is whether anybody can read it yet, and the address is the answer.
896
+ */
897
+ function savedMessage(server, result) {
898
+ const resume = result
899
+ ?.resume;
900
+ const visibility = resume?.visibility;
901
+ const publicSlug = resume?.publicSlug ?? null;
902
+ if (visibility === 'public' && publicSlug !== null) {
903
+ return `Saved and listed: ${server}/candidates/${publicSlug}`;
904
+ }
905
+ if (visibility === 'link' && publicSlug !== null) {
906
+ return `Saved. Anyone with the link: ${server}/candidates/${publicSlug}`;
907
+ }
908
+ if (visibility === 'private') {
909
+ return `Saved, and private. ${dim('agenticjobs resume publish <slug> lists it.')}`;
910
+ }
911
+ return 'Saved.';
912
+ }
641
913
  // --- hiring ---------------------------------------------------------------
642
914
  /**
643
915
  * `new <url>` and `update <url>` are the same call.
@@ -857,6 +1129,127 @@ async function commandFollow(args) {
857
1129
  const result = await clientFor(args).setFollow(candidate ? { candidate: slug } : { org: slug }, following);
858
1130
  return out(args, `${following ? 'Following' : 'Not following'} ${slug}. ${result.followers} ${result.followers === 1 ? 'follower' : 'followers'}.`, result);
859
1131
  }
1132
+ // --- inbox ----------------------------------------------------------------
1133
+ async function commandInbox(args) {
1134
+ const client = clientFor(args);
1135
+ if (args.positional[0] === 'read') {
1136
+ const id = args.positional[1] ?? '';
1137
+ if (id === '') {
1138
+ process.stderr.write('Which one? agenticjobs inbox read <id>\n');
1139
+ return 1;
1140
+ }
1141
+ const result = await client.thread(id);
1142
+ const lines = result.thread.messages.map((message) => [
1143
+ `${bold(message.mine ? 'You' : message.sender.name)} ${dim(ago(message.createdAt))}${message.kind === 'invoice' ? dim(' (invoice)') : ''}`,
1144
+ ` ${message.body.replace(/\n/g, '\n ')}`,
1145
+ ].join('\n'));
1146
+ const invoices = result.invoices.map((invoice) => ` ${invoice.id} $${invoice.amountUsd} ${invoice.currency} ${invoice.status}`);
1147
+ return out(args, [
1148
+ `${bold(result.thread.subject)} ${dim(`with ${result.thread.with.name}`)}`,
1149
+ '',
1150
+ lines.join('\n\n'),
1151
+ ...(invoices.length === 0 ? [] : ['', bold('Invoices'), ...invoices]),
1152
+ ].join('\n'), result);
1153
+ }
1154
+ const result = await client.inbox();
1155
+ if (result.items.length === 0)
1156
+ return out(args, 'Nothing in the inbox.', result);
1157
+ const lines = result.items.map((thread) => [
1158
+ `${bold(thread.with.name)} ${dim(ago(thread.lastMessageAt))}${thread.unread > 0 ? ` ${bold(`${thread.unread} new`)}` : ''}`,
1159
+ ` ${thread.subject}`,
1160
+ ` ${dim(thread.preview)}`,
1161
+ ` ${dim(thread.id)}`,
1162
+ ].join('\n'));
1163
+ return out(args, lines.join('\n\n'), result);
1164
+ }
1165
+ async function commandMessage(args) {
1166
+ const slug = args.positional[0] ?? '';
1167
+ const body = args.positional.slice(1).join(' ');
1168
+ if (slug === '' || body === '') {
1169
+ process.stderr.write('agenticjobs message <employer-slug> <text> [--candidate] [--job <slug>] [--as <employer>]\n');
1170
+ return 1;
1171
+ }
1172
+ const candidate = flagBool(args, 'candidate');
1173
+ const job = flagString(args, 'job');
1174
+ const as = flagString(args, 'as');
1175
+ const subject = flagString(args, 'subject');
1176
+ const result = await clientFor(args).startThread({
1177
+ ...(candidate ? { candidate: slug } : { employer: slug }),
1178
+ ...(job === undefined || job === null ? {} : { job }),
1179
+ ...(as === undefined || as === null ? {} : { as }),
1180
+ ...(subject === undefined || subject === null ? {} : { subject }),
1181
+ body,
1182
+ });
1183
+ return out(args, `${result.created ? 'Sent' : 'Added to the conversation you already have'}. ${result.url}`, result);
1184
+ }
1185
+ async function commandReply(args) {
1186
+ const id = args.positional[0] ?? '';
1187
+ const body = args.positional.slice(1).join(' ');
1188
+ if (id === '' || body === '') {
1189
+ process.stderr.write('agenticjobs reply <thread-id> <text>\n');
1190
+ return 1;
1191
+ }
1192
+ const result = await clientFor(args).reply(id, body);
1193
+ return out(args, 'Sent.', result);
1194
+ }
1195
+ async function commandInvoice(args) {
1196
+ const id = args.positional[0] ?? '';
1197
+ const amount = args.positional[1] ?? '';
1198
+ if (id === '' || amount === '') {
1199
+ process.stderr.write('agenticjobs invoice <thread-id> <amount-usd> [--currency <chain>] [--for <text>]\n');
1200
+ return 1;
1201
+ }
1202
+ const currency = flagString(args, 'currency');
1203
+ const description = flagString(args, 'for');
1204
+ const result = await clientFor(args).sendInvoice(id, {
1205
+ amount,
1206
+ ...(currency === undefined || currency === null ? {} : { currency }),
1207
+ ...(description === undefined || description === null ? {} : { description }),
1208
+ });
1209
+ return out(args, `Invoice sent: $${result.invoice.amountUsd} in ${result.invoice.currency}. ${dim(result.invoice.id)}`, result);
1210
+ }
1211
+ async function commandInvoices(args) {
1212
+ const result = await clientFor(args).invoices();
1213
+ if (result.items.length === 0)
1214
+ return out(args, 'No invoices.', result);
1215
+ const lines = result.items.map((invoice) => `${bold(`$${invoice.amountUsd}`)} ${invoice.currency} ${invoice.status} ${dim(`from ${invoice.payee.name}, ${ago(invoice.createdAt)}`)}\n ${dim(invoice.id)}`);
1216
+ return out(args, lines.join('\n'), result);
1217
+ }
1218
+ async function commandPay(args) {
1219
+ const id = args.positional[0] ?? '';
1220
+ if (id === '') {
1221
+ process.stderr.write('agenticjobs pay <invoice-id>\n');
1222
+ return 1;
1223
+ }
1224
+ const result = await clientFor(args).payInvoice(id);
1225
+ const invoice = result.invoice;
1226
+ if (invoice.status === 'paid')
1227
+ return out(args, 'Already paid.', result);
1228
+ if (invoice.payment === null)
1229
+ return out(args, 'No quote came back. Try again in a moment.', result);
1230
+ return out(args, [
1231
+ `Pay at ${bold(invoice.payment.url)}`,
1232
+ ...(invoice.payment.amountCrypto === null
1233
+ ? []
1234
+ : [` ${invoice.payment.amountCrypto} ${invoice.currency} to ${invoice.payment.address ?? ''}`]),
1235
+ dim(' The quote lasts a few minutes.'),
1236
+ ].join('\n'), result);
1237
+ }
1238
+ async function commandBilling(args) {
1239
+ const result = await clientFor(args).billing();
1240
+ if (!result.configured)
1241
+ return out(args, 'This board has no billing configured.', result);
1242
+ if (result.account === null) {
1243
+ return out(args, `No CoinPay account connected. Connect one in a browser: ${result.connectUrl ?? ''}`, result);
1244
+ }
1245
+ if (!result.account.usable) {
1246
+ return out(args, `The CoinPay connection has lapsed. Reconnect in a browser: ${result.connectUrl ?? ''}`, result);
1247
+ }
1248
+ const wallets = result.account.wallets;
1249
+ return out(args, wallets.length === 0
1250
+ ? 'Connected, but the CoinPay account has no wallet yet.'
1251
+ : `Connected. Can be paid in: ${wallets.map((w) => `${w.chain} ${dim(w.address)}`).join(', ')}.`, result);
1252
+ }
860
1253
  async function commandAnnounce(args) {
861
1254
  const { announceOnce } = await import("../directory/announce.js");
862
1255
  const { loadConfig: serverConfig, sameOrigin } = await import("../config.js");