@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.
- package/README.md +69 -1
- package/dist/cli/args.js +31 -3
- package/dist/cli/args.js.map +1 -1
- package/dist/cli/index.js +396 -3
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/jobfile.js +38 -2
- package/dist/cli/jobfile.js.map +1 -1
- package/dist/client/client.d.ts +149 -0
- package/dist/client/client.js +79 -12
- package/dist/client/client.js.map +1 -1
- package/dist/client/fanout.js +7 -1
- package/dist/client/fanout.js.map +1 -1
- package/dist/config.d.ts +35 -1
- package/dist/config.js +33 -1
- package/dist/config.js.map +1 -1
- package/dist/core/candidates.js +33 -6
- package/dist/core/candidates.js.map +1 -1
- package/dist/core/capacity-alert.d.ts +84 -0
- package/dist/core/capacity-alert.js +144 -0
- package/dist/core/capacity-alert.js.map +1 -0
- package/dist/core/capacity.d.ts +96 -0
- package/dist/core/capacity.js +174 -0
- package/dist/core/capacity.js.map +1 -0
- package/dist/core/coinpay.d.ts +143 -0
- package/dist/core/coinpay.js +417 -0
- package/dist/core/coinpay.js.map +1 -0
- package/dist/core/import-job.js +18 -7
- package/dist/core/import-job.js.map +1 -1
- package/dist/core/import.js +18 -15
- package/dist/core/import.js.map +1 -1
- package/dist/core/inbox.d.ts +155 -0
- package/dist/core/inbox.js +353 -0
- package/dist/core/inbox.js.map +1 -0
- package/dist/core/invoices.d.ts +111 -0
- package/dist/core/invoices.js +292 -0
- package/dist/core/invoices.js.map +1 -0
- package/dist/core/jobs.js +12 -2
- package/dist/core/jobs.js.map +1 -1
- package/dist/core/mail.d.ts +16 -0
- package/dist/core/mail.js +22 -0
- package/dist/core/mail.js.map +1 -1
- package/dist/core/orgs.d.ts +39 -0
- package/dist/core/orgs.js +69 -0
- package/dist/core/orgs.js.map +1 -1
- package/dist/directory/federate.js +2 -4
- package/dist/directory/federate.js.map +1 -1
- package/dist/directory/fetch.d.ts +1 -1
- package/dist/markup/markdown.js +4 -1
- package/dist/markup/markdown.js.map +1 -1
- package/dist/markup/resume.js +66 -1
- package/dist/markup/resume.js.map +1 -1
- package/dist/mcp/server.js +5 -1
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/stdio.js +3 -2
- package/dist/mcp/stdio.js.map +1 -1
- package/dist/mcp/tools.js +151 -1
- package/dist/mcp/tools.js.map +1 -1
- package/dist/schema/jsonld.js +1 -8
- package/dist/schema/jsonld.js.map +1 -1
- package/dist/schema/salary.d.ts +5 -0
- package/dist/schema/salary.js +15 -0
- package/dist/schema/salary.js.map +1 -0
- package/dist/schema/text.js +7 -3
- package/dist/schema/text.js.map +1 -1
- package/dist/server/app.d.ts +4 -3
- package/dist/server/app.js +7 -4
- package/dist/server/app.js.map +1 -1
- package/dist/server/deps.d.ts +5 -0
- package/dist/server/middleware.js +9 -1
- package/dist/server/middleware.js.map +1 -1
- package/dist/server/routes/api.js +347 -1
- package/dist/server/routes/api.js.map +1 -1
- package/dist/server/routes/discovery.js +75 -0
- package/dist/server/routes/discovery.js.map +1 -1
- package/dist/server/routes/inbox.d.ts +9 -0
- package/dist/server/routes/inbox.js +223 -0
- package/dist/server/routes/inbox.js.map +1 -0
- package/dist/server/routes/openapi.js +141 -1
- package/dist/server/routes/openapi.js.map +1 -1
- package/dist/server/routes/pages.d.ts +22 -0
- package/dist/server/routes/pages.js +91 -7
- package/dist/server/routes/pages.js.map +1 -1
- package/dist/views/candidates.d.ts +8 -0
- package/dist/views/candidates.js +2 -1
- package/dist/views/candidates.js.map +1 -1
- package/dist/views/docs.js +48 -4
- package/dist/views/docs.js.map +1 -1
- package/dist/views/inbox.d.ts +90 -0
- package/dist/views/inbox.js +73 -0
- package/dist/views/inbox.js.map +1 -0
- package/dist/views/jobs.d.ts +9 -0
- package/dist/views/jobs.js +3 -2
- package/dist/views/jobs.js.map +1 -1
- package/dist/views/layout.d.ts +2 -0
- package/dist/views/layout.js +2 -2
- package/dist/views/layout.js.map +1 -1
- package/dist/views/me.d.ts +3 -3
- package/dist/views/me.js +2 -2
- package/dist/views/me.js.map +1 -1
- package/dist/views/updates.d.ts +13 -0
- package/dist/views/updates.js +2 -1
- package/dist/views/updates.js.map +1 -1
- package/docs/openjob.md +5 -0
- package/docs/openresume.md +36 -0
- package/migrations/0014_inbox_and_billing.sql +138 -0
- package/package.json +15 -17
- package/web/public/app.css +146 -3
- 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
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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')
|
package/dist/cli/args.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,KAAK,GAAgD,EAAE,CAAC;IAC9D,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAEhC,IAAI,OAAO,EAAE,CAAC;YACZ,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,SAAS;QACX,CAAC;QACD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO,GAAG,IAAI,CAAC;YACf,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;gBAChB,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;gBAC1D,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YAC7B,IAAI,
|
|
1
|
+
{"version":3,"file":"args.js","sourceRoot":"","sources":["../../src/cli/args.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAQH,yEAAyE;AACzE,0EAA0E;AAC1E,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC;IAC5B,SAAS;IACT,GAAG;IACH,MAAM;IACN,GAAG;IACH,KAAK;IACL,GAAG;IACH,MAAM;IACN,QAAQ;IACR,QAAQ;IACR,KAAK;IACL,SAAS;IACT,cAAc;IACd,OAAO;IACP,eAAe;IACf,SAAS;IACT,WAAW;IACX,WAAW;CACZ,CAAC,CAAC;AAEH,SAAS,UAAU,CAAC,IAAY,EAAE,IAAwB;IACxD,OAAO,CACL,IAAI,KAAK,SAAS;QAClB,CAAC,IAAI,CAAC,UAAU,CAAC,GAAG,CAAC;QACrB,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,4BAA4B,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CACtE,CAAC;AACJ,CAAC;AAED,MAAM,UAAU,SAAS,CAAC,IAAc;IACtC,MAAM,UAAU,GAAa,EAAE,CAAC;IAChC,MAAM,KAAK,GAAgD,EAAE,CAAC;IAC9D,IAAI,OAAO,GAAG,KAAK,CAAC;IAEpB,KAAK,IAAI,KAAK,GAAG,CAAC,EAAE,KAAK,GAAG,IAAI,CAAC,MAAM,EAAE,KAAK,IAAI,CAAC,EAAE,CAAC;QACpD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,EAAE,CAAC;QAEhC,IAAI,OAAO,EAAE,CAAC;YACZ,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACvB,SAAS;QACX,CAAC;QACD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;YACnB,OAAO,GAAG,IAAI,CAAC;YACf,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAC3B,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;YACjC,IAAI,MAAM,IAAI,CAAC,EAAE,CAAC;gBAChB,GAAG,CAAC,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,EAAE,MAAM,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC;gBAC1D,SAAS;YACX,CAAC;YACD,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YAC7B,IAAI,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;gBAC3B,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;gBACvB,KAAK,IAAI,CAAC,CAAC;gBACX,SAAS;YACX,CAAC;YACD,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YACvB,SAAS;QACX,CAAC;QAED,IAAI,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;YAC9C,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAC5B,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC;YAC7B,IAAI,UAAU,CAAC,IAAI,EAAE,IAAI,CAAC,EAAE,CAAC;gBAC3B,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;gBACvB,KAAK,IAAI,CAAC,CAAC;gBACX,SAAS;YACX,CAAC;YACD,GAAG,CAAC,KAAK,EAAE,IAAI,EAAE,IAAI,CAAC,CAAC;YACvB,SAAS;QACX,CAAC;QAED,UAAU,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;IACzB,CAAC;IAED,OAAO,EAAE,OAAO,EAAE,UAAU,CAAC,KAAK,EAAE,IAAI,EAAE,EAAE,UAAU,EAAE,KAAK,EAAE,CAAC;AAClE,CAAC;AAED,SAAS,GAAG,CACV,KAAkD,EAClD,GAAW,EACX,KAAuB;IAEvB,MAAM,QAAQ,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC;IAC5B,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;QAC3B,KAAK,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;QACnB,OAAO;IACT,CAAC;IACD,2EAA2E;IAC3E,6DAA6D;IAC7D,MAAM,IAAI,GAAG,KAAK,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC;IACrE,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;IACzB,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC;AACpB,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAU,EAAE,GAAG,KAAe;IACvD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,KAAK,CAAC;QAC5C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,OAAO,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAC3D,CAAC;IACD,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAU,EAAE,GAAG,KAAe;IACrD,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC7B,+DAA+D;QAC/D,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;QAC7D,IAAI,KAAK,KAAK,IAAI;YAAE,OAAO,IAAI,CAAC;QAChC,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,OAAO,CAAC,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC,CAAC,QAAQ,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;IAC5F,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,QAAQ,CAAC,IAAU,EAAE,GAAG,KAAe;IACrD,MAAM,GAAG,GAAa,EAAE,CAAC;IACzB,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;QAC/B,IAAI,OAAO,KAAK,KAAK,QAAQ;YAAE,GAAG,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;aAC1C,IAAI,KAAK,CAAC,OAAO,CAAC,KAAK,CAAC;YAAE,GAAG,CAAC,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;IACpD,CAAC;IACD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,UAAU,CAAC,IAAU,EAAE,GAAG,KAAe;IACvD,MAAM,GAAG,GAAG,UAAU,CAAC,IAAI,EAAE,GAAG,KAAK,CAAC,CAAC;IACvC,IAAI,GAAG,KAAK,SAAS;QAAE,OAAO,SAAS,CAAC;IACxC,MAAM,MAAM,GAAG,MAAM,CAAC,QAAQ,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC;IACxC,OAAO,MAAM,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS,CAAC;AACtD,CAAC"}
|
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,
|
|
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}
|
|
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");
|