@profullstack/agenticjobs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +219 -0
- package/bin/agenticjobs-mcp.mjs +2 -0
- package/bin/agenticjobs.mjs +2 -0
- package/dist/cli/args.d.ts +17 -0
- package/dist/cli/args.js +103 -0
- package/dist/cli/args.js.map +1 -0
- package/dist/cli/format.d.ts +15 -0
- package/dist/cli/format.js +24 -0
- package/dist/cli/format.js.map +1 -0
- package/dist/cli/index.d.ts +13 -0
- package/dist/cli/index.js +620 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/jobfile.d.ts +15 -0
- package/dist/cli/jobfile.js +94 -0
- package/dist/cli/jobfile.js.map +1 -0
- package/dist/client/client.d.ts +105 -0
- package/dist/client/client.js +182 -0
- package/dist/client/client.js.map +1 -0
- package/dist/client/config.d.ts +33 -0
- package/dist/client/config.js +92 -0
- package/dist/client/config.js.map +1 -0
- package/dist/client/fanout.d.ts +35 -0
- package/dist/client/fanout.js +77 -0
- package/dist/client/fanout.js.map +1 -0
- package/dist/client/index.d.ts +4 -0
- package/dist/client/index.js +5 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/login.d.ts +23 -0
- package/dist/client/login.js +45 -0
- package/dist/client/login.js.map +1 -0
- package/dist/config.d.ts +40 -0
- package/dist/config.js +64 -0
- package/dist/config.js.map +1 -0
- package/dist/core/applications.d.ts +67 -0
- package/dist/core/applications.js +187 -0
- package/dist/core/applications.js.map +1 -0
- package/dist/core/auth.d.ts +120 -0
- package/dist/core/auth.js +246 -0
- package/dist/core/auth.js.map +1 -0
- package/dist/core/import.d.ts +52 -0
- package/dist/core/import.js +311 -0
- package/dist/core/import.js.map +1 -0
- package/dist/core/jobs.d.ts +91 -0
- package/dist/core/jobs.js +381 -0
- package/dist/core/jobs.js.map +1 -0
- package/dist/core/orgs.d.ts +21 -0
- package/dist/core/orgs.js +111 -0
- package/dist/core/orgs.js.map +1 -0
- package/dist/core/resumes.d.ts +51 -0
- package/dist/core/resumes.js +144 -0
- package/dist/core/resumes.js.map +1 -0
- package/dist/core/zip.d.ts +17 -0
- package/dist/core/zip.js +77 -0
- package/dist/core/zip.js.map +1 -0
- package/dist/db/index.d.ts +3 -0
- package/dist/db/index.js +3 -0
- package/dist/db/index.js.map +1 -0
- package/dist/db/migrate.d.ts +15 -0
- package/dist/db/migrate.js +67 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/pool.d.ts +13 -0
- package/dist/db/pool.js +68 -0
- package/dist/db/pool.js.map +1 -0
- package/dist/db/seed.d.ts +9 -0
- package/dist/db/seed.js +120 -0
- package/dist/db/seed.js.map +1 -0
- package/dist/directory/announce.d.ts +32 -0
- package/dist/directory/announce.js +59 -0
- package/dist/directory/announce.js.map +1 -0
- package/dist/directory/federate.d.ts +59 -0
- package/dist/directory/federate.js +126 -0
- package/dist/directory/federate.js.map +1 -0
- package/dist/directory/fetch.d.ts +35 -0
- package/dist/directory/fetch.js +138 -0
- package/dist/directory/fetch.js.map +1 -0
- package/dist/directory/index.d.ts +4 -0
- package/dist/directory/index.js +5 -0
- package/dist/directory/index.js.map +1 -0
- package/dist/directory/registry.d.ts +55 -0
- package/dist/directory/registry.js +155 -0
- package/dist/directory/registry.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -0
- package/dist/markup/escape.d.ts +20 -0
- package/dist/markup/escape.js +56 -0
- package/dist/markup/escape.js.map +1 -0
- package/dist/markup/markdown.d.ts +35 -0
- package/dist/markup/markdown.js +279 -0
- package/dist/markup/markdown.js.map +1 -0
- package/dist/markup/resume.d.ts +72 -0
- package/dist/markup/resume.js +260 -0
- package/dist/markup/resume.js.map +1 -0
- package/dist/mcp/protocol.d.ts +56 -0
- package/dist/mcp/protocol.js +48 -0
- package/dist/mcp/protocol.js.map +1 -0
- package/dist/mcp/server.d.ts +21 -0
- package/dist/mcp/server.js +73 -0
- package/dist/mcp/server.js.map +1 -0
- package/dist/mcp/stdio.d.ts +13 -0
- package/dist/mcp/stdio.js +85 -0
- package/dist/mcp/stdio.js.map +1 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.js +351 -0
- package/dist/mcp/tools.js.map +1 -0
- package/dist/schema/index.d.ts +5 -0
- package/dist/schema/index.js +6 -0
- package/dist/schema/index.js.map +1 -0
- package/dist/schema/instance.d.ts +85 -0
- package/dist/schema/instance.js +147 -0
- package/dist/schema/instance.js.map +1 -0
- package/dist/schema/job.d.ts +151 -0
- package/dist/schema/job.js +73 -0
- package/dist/schema/job.js.map +1 -0
- package/dist/schema/jsonld.d.ts +18 -0
- package/dist/schema/jsonld.js +120 -0
- package/dist/schema/jsonld.js.map +1 -0
- package/dist/schema/query.d.ts +47 -0
- package/dist/schema/query.js +103 -0
- package/dist/schema/query.js.map +1 -0
- package/dist/schema/text.d.ts +27 -0
- package/dist/schema/text.js +117 -0
- package/dist/schema/text.js.map +1 -0
- package/dist/server/app.d.ts +13 -0
- package/dist/server/app.js +57 -0
- package/dist/server/app.js.map +1 -0
- package/dist/server/deps.d.ts +20 -0
- package/dist/server/deps.js +2 -0
- package/dist/server/deps.js.map +1 -0
- package/dist/server/middleware.d.ts +39 -0
- package/dist/server/middleware.js +100 -0
- package/dist/server/middleware.js.map +1 -0
- package/dist/server/routes/api.d.ts +20 -0
- package/dist/server/routes/api.js +584 -0
- package/dist/server/routes/api.js.map +1 -0
- package/dist/server/routes/descriptor.d.ts +11 -0
- package/dist/server/routes/descriptor.js +31 -0
- package/dist/server/routes/descriptor.js.map +1 -0
- package/dist/server/routes/discovery.d.ts +12 -0
- package/dist/server/routes/discovery.js +305 -0
- package/dist/server/routes/discovery.js.map +1 -0
- package/dist/server/routes/mcp.d.ts +19 -0
- package/dist/server/routes/mcp.js +85 -0
- package/dist/server/routes/mcp.js.map +1 -0
- package/dist/server/routes/openapi.d.ts +11 -0
- package/dist/server/routes/openapi.js +297 -0
- package/dist/server/routes/openapi.js.map +1 -0
- package/dist/server/routes/pages.d.ts +11 -0
- package/dist/server/routes/pages.js +445 -0
- package/dist/server/routes/pages.js.map +1 -0
- package/dist/server/routes/passkey.d.ts +20 -0
- package/dist/server/routes/passkey.js +198 -0
- package/dist/server/routes/passkey.js.map +1 -0
- package/dist/server/routes/specs.d.ts +10 -0
- package/dist/server/routes/specs.js +42 -0
- package/dist/server/routes/specs.js.map +1 -0
- package/dist/server/serve.d.ts +11 -0
- package/dist/server/serve.js +79 -0
- package/dist/server/serve.js.map +1 -0
- package/dist/tui/index.d.ts +13 -0
- package/dist/tui/index.js +266 -0
- package/dist/tui/index.js.map +1 -0
- package/dist/tui/state.d.ts +63 -0
- package/dist/tui/state.js +136 -0
- package/dist/tui/state.js.map +1 -0
- package/dist/tui/types.d.ts +2 -0
- package/dist/tui/types.js +2 -0
- package/dist/tui/types.js.map +1 -0
- package/dist/tui/views.d.ts +11 -0
- package/dist/tui/views.js +197 -0
- package/dist/tui/views.js.map +1 -0
- package/dist/views/auth.d.ts +21 -0
- package/dist/views/auth.js +5 -0
- package/dist/views/auth.js.map +1 -0
- package/dist/views/docs.d.ts +17 -0
- package/dist/views/docs.js +40 -0
- package/dist/views/docs.js.map +1 -0
- package/dist/views/jobs.d.ts +49 -0
- package/dist/views/jobs.js +44 -0
- package/dist/views/jobs.js.map +1 -0
- package/dist/views/layout.d.ts +58 -0
- package/dist/views/layout.js +36 -0
- package/dist/views/layout.js.map +1 -0
- package/dist/views/me.d.ts +27 -0
- package/dist/views/me.js +6 -0
- package/dist/views/me.js.map +1 -0
- package/dist/views/network.d.ts +21 -0
- package/dist/views/network.js +17 -0
- package/dist/views/network.js.map +1 -0
- package/dist/views/post.d.ts +22 -0
- package/dist/views/post.js +20 -0
- package/dist/views/post.js.map +1 -0
- package/docs/openjob.md +200 -0
- package/docs/openresume.md +143 -0
- package/migrations/0001_init.sql +109 -0
- package/migrations/0002_auth.sql +82 -0
- package/migrations/0003_directory.sql +27 -0
- package/migrations/0004_resumes.sql +63 -0
- package/migrations/0005_application_drafts.sql +20 -0
- package/package.json +92 -0
- package/web/public/app.css +731 -0
- package/web/public/app.js +173 -0
- package/web/public/icon.svg +5 -0
- package/web/public/sw.js +64 -0
- package/web/public/tokens.css +153 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The heartbeat, from the instance's side.
|
|
3
|
+
*
|
|
4
|
+
* An instance sends one field — its own URL — and the directory goes and reads
|
|
5
|
+
* the rest for itself. That asymmetry is the whole design: there is nothing in
|
|
6
|
+
* an announcement worth forging, so there is nothing to authenticate.
|
|
7
|
+
*
|
|
8
|
+
* Announcing is opt-in. An instance with ANNOUNCE unset never contacts
|
|
9
|
+
* anything, which is the correct default for a company running this for its
|
|
10
|
+
* own openings.
|
|
11
|
+
*/
|
|
12
|
+
export interface Announcer {
|
|
13
|
+
stop: () => void;
|
|
14
|
+
/** Exposed so the CLI can announce once and exit. */
|
|
15
|
+
beat: () => Promise<boolean>;
|
|
16
|
+
}
|
|
17
|
+
export interface AnnounceOptions {
|
|
18
|
+
directoryUrl: string;
|
|
19
|
+
publicUrl: string;
|
|
20
|
+
intervalMs?: number;
|
|
21
|
+
log?: (message: string) => void;
|
|
22
|
+
}
|
|
23
|
+
export declare function announceOnce(directoryUrl: string, publicUrl: string): Promise<void>;
|
|
24
|
+
/**
|
|
25
|
+
* Announce now, then keep announcing.
|
|
26
|
+
*
|
|
27
|
+
* Failures are logged once and retried on the next beat rather than escalated:
|
|
28
|
+
* a directory being down is not a reason for a job board to stop serving jobs,
|
|
29
|
+
* and the only consequence is that the instance drops off a list until it
|
|
30
|
+
* comes back.
|
|
31
|
+
*/
|
|
32
|
+
export declare function startAnnouncing(options: AnnounceOptions): Announcer;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The heartbeat, from the instance's side.
|
|
3
|
+
*
|
|
4
|
+
* An instance sends one field — its own URL — and the directory goes and reads
|
|
5
|
+
* the rest for itself. That asymmetry is the whole design: there is nothing in
|
|
6
|
+
* an announcement worth forging, so there is nothing to authenticate.
|
|
7
|
+
*
|
|
8
|
+
* Announcing is opt-in. An instance with ANNOUNCE unset never contacts
|
|
9
|
+
* anything, which is the correct default for a company running this for its
|
|
10
|
+
* own openings.
|
|
11
|
+
*/
|
|
12
|
+
import { HEARTBEAT_MS } from "../schema/instance.js";
|
|
13
|
+
import { fetchJson, FetchProblem } from "./fetch.js";
|
|
14
|
+
export async function announceOnce(directoryUrl, publicUrl) {
|
|
15
|
+
await fetchJson(`${directoryUrl.replace(/\/+$/, '')}/api/v1/directory/announce`, {
|
|
16
|
+
method: 'POST',
|
|
17
|
+
body: { url: publicUrl },
|
|
18
|
+
maxBytes: 64 * 1024,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Announce now, then keep announcing.
|
|
23
|
+
*
|
|
24
|
+
* Failures are logged once and retried on the next beat rather than escalated:
|
|
25
|
+
* a directory being down is not a reason for a job board to stop serving jobs,
|
|
26
|
+
* and the only consequence is that the instance drops off a list until it
|
|
27
|
+
* comes back.
|
|
28
|
+
*/
|
|
29
|
+
export function startAnnouncing(options) {
|
|
30
|
+
const interval = options.intervalMs ?? HEARTBEAT_MS;
|
|
31
|
+
const log = options.log ?? ((message) => console.log(message));
|
|
32
|
+
let lastError = null;
|
|
33
|
+
const beat = async () => {
|
|
34
|
+
try {
|
|
35
|
+
await announceOnce(options.directoryUrl, options.publicUrl);
|
|
36
|
+
if (lastError !== null) {
|
|
37
|
+
log(`directory: ${options.directoryUrl} is answering again`);
|
|
38
|
+
lastError = null;
|
|
39
|
+
}
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
catch (error) {
|
|
43
|
+
const message = error instanceof FetchProblem ? error.message : String(error);
|
|
44
|
+
// Only the first occurrence of a given failure is logged. A directory
|
|
45
|
+
// that is down for a day would otherwise write 144 identical lines.
|
|
46
|
+
if (message !== lastError) {
|
|
47
|
+
log(`directory: could not announce to ${options.directoryUrl}: ${message}`);
|
|
48
|
+
lastError = message;
|
|
49
|
+
}
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
void beat();
|
|
54
|
+
const timer = setInterval(() => void beat(), interval);
|
|
55
|
+
// Announcing must never be the reason a process stays alive.
|
|
56
|
+
timer.unref?.();
|
|
57
|
+
return { stop: () => clearInterval(timer), beat };
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=announce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"announce.js","sourceRoot":"","sources":["../../src/directory/announce.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AACrD,OAAO,EAAE,SAAS,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAerD,MAAM,CAAC,KAAK,UAAU,YAAY,CAAC,YAAoB,EAAE,SAAiB;IACxE,MAAM,SAAS,CAAC,GAAG,YAAY,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,4BAA4B,EAAE;QAC/E,MAAM,EAAE,MAAM;QACd,IAAI,EAAE,EAAE,GAAG,EAAE,SAAS,EAAE;QACxB,QAAQ,EAAE,EAAE,GAAG,IAAI;KACpB,CAAC,CAAC;AACL,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,UAAU,eAAe,CAAC,OAAwB;IACtD,MAAM,QAAQ,GAAG,OAAO,CAAC,UAAU,IAAI,YAAY,CAAC;IACpD,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,CAAC,OAAe,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC;IACvE,IAAI,SAAS,GAAkB,IAAI,CAAC;IAEpC,MAAM,IAAI,GAAG,KAAK,IAAsB,EAAE;QACxC,IAAI,CAAC;YACH,MAAM,YAAY,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,SAAS,CAAC,CAAC;YAC5D,IAAI,SAAS,KAAK,IAAI,EAAE,CAAC;gBACvB,GAAG,CAAC,cAAc,OAAO,CAAC,YAAY,qBAAqB,CAAC,CAAC;gBAC7D,SAAS,GAAG,IAAI,CAAC;YACnB,CAAC;YACD,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,OAAO,GAAG,KAAK,YAAY,YAAY,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;YAC9E,sEAAsE;YACtE,oEAAoE;YACpE,IAAI,OAAO,KAAK,SAAS,EAAE,CAAC;gBAC1B,GAAG,CAAC,oCAAoC,OAAO,CAAC,YAAY,KAAK,OAAO,EAAE,CAAC,CAAC;gBAC5E,SAAS,GAAG,OAAO,CAAC;YACtB,CAAC;YACD,OAAO,KAAK,CAAC;QACf,CAAC;IACH,CAAC,CAAC;IAEF,KAAK,IAAI,EAAE,CAAC;IACZ,MAAM,KAAK,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC,KAAK,IAAI,EAAE,EAAE,QAAQ,CAAC,CAAC;IACvD,6DAA6D;IAC7D,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;IAEhB,OAAO,EAAE,IAAI,EAAE,GAAG,EAAE,CAAC,aAAa,CAAC,KAAK,CAAC,EAAE,IAAI,EAAE,CAAC;AACpD,CAAC"}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Searching more than one instance at once.
|
|
3
|
+
*
|
|
4
|
+
* This is what the directory is for. A candidate should be able to ask one
|
|
5
|
+
* question and have it answered by every board that opted in, without any of
|
|
6
|
+
* those boards having to share a database, and without a slow or hostile one
|
|
7
|
+
* being able to hold up the answer.
|
|
8
|
+
*
|
|
9
|
+
* The rules that make that true:
|
|
10
|
+
*
|
|
11
|
+
* - every instance is queried in parallel, with its own timeout;
|
|
12
|
+
* - one instance failing removes that instance from the results and nothing
|
|
13
|
+
* else, and the failure is reported rather than swallowed, so a client can
|
|
14
|
+
* say "11 of 12 boards answered" instead of quietly showing less;
|
|
15
|
+
* - results carry the instance they came from, because a job on someone
|
|
16
|
+
* else's board is applied for on someone else's board.
|
|
17
|
+
*/
|
|
18
|
+
import type { Job, JobQuery } from '../schema/index.ts';
|
|
19
|
+
import type { InstanceDescriptor } from '../schema/instance.ts';
|
|
20
|
+
export interface FederatedJob {
|
|
21
|
+
job: Job;
|
|
22
|
+
/** Origin of the instance that served it. */
|
|
23
|
+
instance: string;
|
|
24
|
+
instanceName: string;
|
|
25
|
+
/** Where a human or an agent goes to apply. Always on the origin board. */
|
|
26
|
+
url: string;
|
|
27
|
+
}
|
|
28
|
+
export interface FederatedSearch {
|
|
29
|
+
jobs: FederatedJob[];
|
|
30
|
+
/** One entry per instance queried, in the order they were asked. */
|
|
31
|
+
sources: SourceResult[];
|
|
32
|
+
/** Sum of `total` across the instances that answered. */
|
|
33
|
+
total: number;
|
|
34
|
+
}
|
|
35
|
+
export interface SourceResult {
|
|
36
|
+
instance: string;
|
|
37
|
+
name: string;
|
|
38
|
+
ok: boolean;
|
|
39
|
+
count: number;
|
|
40
|
+
total: number;
|
|
41
|
+
ms: number;
|
|
42
|
+
error: string | null;
|
|
43
|
+
}
|
|
44
|
+
export interface FederateOptions {
|
|
45
|
+
timeoutMs?: number;
|
|
46
|
+
concurrency?: number;
|
|
47
|
+
/** Cap per instance, so one busy board cannot crowd out eleven others. */
|
|
48
|
+
perInstance?: number;
|
|
49
|
+
signal?: AbortSignal;
|
|
50
|
+
}
|
|
51
|
+
interface Target {
|
|
52
|
+
url: string;
|
|
53
|
+
name: string;
|
|
54
|
+
search: string;
|
|
55
|
+
}
|
|
56
|
+
export declare function targetsFromDescriptors(descriptors: InstanceDescriptor[]): Target[];
|
|
57
|
+
export declare function targetsFromUrls(urls: string[]): Target[];
|
|
58
|
+
export declare function federatedSearch(targets: Target[], query: JobQuery, options?: FederateOptions): Promise<FederatedSearch>;
|
|
59
|
+
export {};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Searching more than one instance at once.
|
|
3
|
+
*
|
|
4
|
+
* This is what the directory is for. A candidate should be able to ask one
|
|
5
|
+
* question and have it answered by every board that opted in, without any of
|
|
6
|
+
* those boards having to share a database, and without a slow or hostile one
|
|
7
|
+
* being able to hold up the answer.
|
|
8
|
+
*
|
|
9
|
+
* The rules that make that true:
|
|
10
|
+
*
|
|
11
|
+
* - every instance is queried in parallel, with its own timeout;
|
|
12
|
+
* - one instance failing removes that instance from the results and nothing
|
|
13
|
+
* else, and the failure is reported rather than swallowed, so a client can
|
|
14
|
+
* say "11 of 12 boards answered" instead of quietly showing less;
|
|
15
|
+
* - results carry the instance they came from, because a job on someone
|
|
16
|
+
* else's board is applied for on someone else's board.
|
|
17
|
+
*/
|
|
18
|
+
import { queryToParams } from "../schema/query.js";
|
|
19
|
+
import { fetchJson, FetchProblem } from "./fetch.js";
|
|
20
|
+
export function targetsFromDescriptors(descriptors) {
|
|
21
|
+
return descriptors.map((descriptor) => ({
|
|
22
|
+
url: descriptor.url,
|
|
23
|
+
name: descriptor.name,
|
|
24
|
+
search: descriptor.endpoints.search,
|
|
25
|
+
}));
|
|
26
|
+
}
|
|
27
|
+
export function targetsFromUrls(urls) {
|
|
28
|
+
return urls.map((url) => {
|
|
29
|
+
const origin = url.replace(/\/+$/, '');
|
|
30
|
+
return { url: origin, name: hostOf(origin), search: `${origin}/api/v1/jobs` };
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function hostOf(url) {
|
|
34
|
+
try {
|
|
35
|
+
return new URL(url).hostname;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return url;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
export async function federatedSearch(targets, query, options = {}) {
|
|
42
|
+
const perInstance = Math.min(100, Math.max(1, options.perInstance ?? query.limit));
|
|
43
|
+
const concurrency = Math.min(24, Math.max(1, options.concurrency ?? 8));
|
|
44
|
+
const params = queryToParams({ ...query, limit: perInstance, offset: 0 });
|
|
45
|
+
const sources = targets.map((target) => ({
|
|
46
|
+
instance: target.url,
|
|
47
|
+
name: target.name,
|
|
48
|
+
ok: false,
|
|
49
|
+
count: 0,
|
|
50
|
+
total: 0,
|
|
51
|
+
ms: 0,
|
|
52
|
+
error: null,
|
|
53
|
+
}));
|
|
54
|
+
const collected = [];
|
|
55
|
+
let cursor = 0;
|
|
56
|
+
const workers = Array.from({ length: Math.min(concurrency, targets.length) }, async () => {
|
|
57
|
+
for (;;) {
|
|
58
|
+
const index = cursor;
|
|
59
|
+
cursor += 1;
|
|
60
|
+
const target = targets[index];
|
|
61
|
+
const source = sources[index];
|
|
62
|
+
if (target === undefined || source === undefined)
|
|
63
|
+
return;
|
|
64
|
+
const started = Date.now();
|
|
65
|
+
try {
|
|
66
|
+
const payload = await fetchJson(`${target.search}?${params.toString()}`, {
|
|
67
|
+
...(options.timeoutMs === undefined ? {} : { timeoutMs: options.timeoutMs }),
|
|
68
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
69
|
+
});
|
|
70
|
+
const page = payload;
|
|
71
|
+
const items = Array.isArray(page.items) ? page.items : [];
|
|
72
|
+
for (const item of items.slice(0, perInstance)) {
|
|
73
|
+
const job = item;
|
|
74
|
+
// A malformed row from one instance drops that row, not the answer.
|
|
75
|
+
if (typeof job?.slug !== 'string' || typeof job?.title !== 'string')
|
|
76
|
+
continue;
|
|
77
|
+
collected.push({
|
|
78
|
+
job,
|
|
79
|
+
instance: target.url,
|
|
80
|
+
instanceName: target.name,
|
|
81
|
+
url: `${target.url}/jobs/${job.slug}`,
|
|
82
|
+
});
|
|
83
|
+
source.count += 1;
|
|
84
|
+
}
|
|
85
|
+
source.total = Number(page.total) || source.count;
|
|
86
|
+
source.ok = true;
|
|
87
|
+
}
|
|
88
|
+
catch (error) {
|
|
89
|
+
source.error = error instanceof FetchProblem ? error.message : String(error);
|
|
90
|
+
}
|
|
91
|
+
finally {
|
|
92
|
+
source.ms = Date.now() - started;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
await Promise.all(workers);
|
|
97
|
+
sortMerged(collected, query);
|
|
98
|
+
return {
|
|
99
|
+
jobs: collected.slice(query.offset, query.offset + query.limit),
|
|
100
|
+
sources,
|
|
101
|
+
total: sources.reduce((sum, source) => sum + (source.ok ? source.total : 0), 0),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Merge-sort the combined set.
|
|
106
|
+
*
|
|
107
|
+
* Each instance already sorted its own page, but "recent" across twelve boards
|
|
108
|
+
* is not any one board's order, so it has to be redone here. Relevance cannot
|
|
109
|
+
* be: the rank came from each instance's own corpus statistics and the numbers
|
|
110
|
+
* are not comparable between them, so a relevance search falls back to recency
|
|
111
|
+
* once merged rather than pretending otherwise.
|
|
112
|
+
*/
|
|
113
|
+
function sortMerged(jobs, query) {
|
|
114
|
+
if (query.sort === 'salary') {
|
|
115
|
+
jobs.sort((a, b) => topSalary(b.job) - topSalary(a.job));
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
jobs.sort((a, b) => published(b.job) - published(a.job));
|
|
119
|
+
}
|
|
120
|
+
function published(job) {
|
|
121
|
+
return Date.parse(job.publishedAt ?? job.createdAt) || 0;
|
|
122
|
+
}
|
|
123
|
+
function topSalary(job) {
|
|
124
|
+
return job.salary.max ?? job.salary.min ?? 0;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=federate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"federate.js","sourceRoot":"","sources":["../../src/directory/federate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetching from another instance.
|
|
3
|
+
*
|
|
4
|
+
* Everything here talks to a URL a stranger supplied, so the rules are the
|
|
5
|
+
* same in all three directions (a directory reading an instance, an instance
|
|
6
|
+
* announcing to a directory, a client fanning a search out): a short timeout,
|
|
7
|
+
* a size cap, no redirect to a different origin, and no trust in anything the
|
|
8
|
+
* response says about where it came from.
|
|
9
|
+
*/
|
|
10
|
+
import { type InstanceDescriptor } from '../schema/instance.ts';
|
|
11
|
+
export declare const USER_AGENT = "agenticjobs/0.1.0 (+https://agenticjobs.work)";
|
|
12
|
+
export declare class FetchProblem extends Error {
|
|
13
|
+
}
|
|
14
|
+
export interface FetchOptions {
|
|
15
|
+
timeoutMs?: number;
|
|
16
|
+
maxBytes?: number;
|
|
17
|
+
signal?: AbortSignal;
|
|
18
|
+
method?: 'GET' | 'POST';
|
|
19
|
+
body?: unknown;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* A JSON GET with the safety rails on.
|
|
23
|
+
*
|
|
24
|
+
* Redirects are followed manually so that a redirect off the origin can be
|
|
25
|
+
* refused: allowing one would let any listed instance point the directory's
|
|
26
|
+
* HTTP client at an address the origin check already rejected.
|
|
27
|
+
*/
|
|
28
|
+
export declare function fetchJson(url: string, options?: FetchOptions): Promise<unknown>;
|
|
29
|
+
/**
|
|
30
|
+
* Read an instance's descriptor from the instance itself.
|
|
31
|
+
*
|
|
32
|
+
* The URL it is parsed against is where we fetched it, never what the document
|
|
33
|
+
* claims, so an instance cannot announce itself as somebody else.
|
|
34
|
+
*/
|
|
35
|
+
export declare function fetchDescriptor(origin: string, options?: FetchOptions): Promise<InstanceDescriptor>;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fetching from another instance.
|
|
3
|
+
*
|
|
4
|
+
* Everything here talks to a URL a stranger supplied, so the rules are the
|
|
5
|
+
* same in all three directions (a directory reading an instance, an instance
|
|
6
|
+
* announcing to a directory, a client fanning a search out): a short timeout,
|
|
7
|
+
* a size cap, no redirect to a different origin, and no trust in anything the
|
|
8
|
+
* response says about where it came from.
|
|
9
|
+
*/
|
|
10
|
+
import { publishable, WELL_KNOWN_PATH } from "../schema/instance.js";
|
|
11
|
+
import { parseDescriptor } from "../schema/instance.js";
|
|
12
|
+
import { SOFTWARE_NAME, VERSION } from "../config.js";
|
|
13
|
+
export const USER_AGENT = `${SOFTWARE_NAME}/${VERSION} (+https://agenticjobs.work)`;
|
|
14
|
+
/** Big enough for a page of jobs, small enough that nobody can wedge a peer. */
|
|
15
|
+
const MAX_BYTES = 2 * 1024 * 1024;
|
|
16
|
+
const TIMEOUT_MS = 8000;
|
|
17
|
+
export class FetchProblem extends Error {
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A JSON GET with the safety rails on.
|
|
21
|
+
*
|
|
22
|
+
* Redirects are followed manually so that a redirect off the origin can be
|
|
23
|
+
* refused: allowing one would let any listed instance point the directory's
|
|
24
|
+
* HTTP client at an address the origin check already rejected.
|
|
25
|
+
*/
|
|
26
|
+
export async function fetchJson(url, options = {}) {
|
|
27
|
+
const origin = publishable(url);
|
|
28
|
+
if (origin === null)
|
|
29
|
+
throw new FetchProblem(`${url} is not an address we will fetch`);
|
|
30
|
+
const controller = new AbortController();
|
|
31
|
+
const timer = setTimeout(() => controller.abort(), options.timeoutMs ?? TIMEOUT_MS);
|
|
32
|
+
const abort = () => controller.abort();
|
|
33
|
+
options.signal?.addEventListener('abort', abort, { once: true });
|
|
34
|
+
try {
|
|
35
|
+
let target = url;
|
|
36
|
+
for (let hop = 0; hop < 3; hop += 1) {
|
|
37
|
+
const response = await fetch(target, {
|
|
38
|
+
method: options.method ?? 'GET',
|
|
39
|
+
redirect: 'manual',
|
|
40
|
+
signal: controller.signal,
|
|
41
|
+
headers: {
|
|
42
|
+
accept: 'application/json',
|
|
43
|
+
'user-agent': USER_AGENT,
|
|
44
|
+
...(options.body === undefined ? {} : { 'content-type': 'application/json' }),
|
|
45
|
+
},
|
|
46
|
+
...(options.body === undefined ? {} : { body: JSON.stringify(options.body) }),
|
|
47
|
+
});
|
|
48
|
+
if (response.status >= 300 && response.status < 400) {
|
|
49
|
+
const location = response.headers.get('location');
|
|
50
|
+
if (location === null)
|
|
51
|
+
throw new FetchProblem(`${target} redirected with no location`);
|
|
52
|
+
const next = new URL(location, target);
|
|
53
|
+
if (next.origin !== origin.origin) {
|
|
54
|
+
throw new FetchProblem(`${target} redirected off its own origin, to ${next.origin}`);
|
|
55
|
+
}
|
|
56
|
+
target = next.toString();
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (!response.ok)
|
|
60
|
+
throw new FetchProblem(`${target} answered ${response.status}`);
|
|
61
|
+
// Content-Length is a hint from the other end, so the body is measured
|
|
62
|
+
// as it arrives rather than trusted up front.
|
|
63
|
+
const declared = Number(response.headers.get('content-length') ?? '0');
|
|
64
|
+
const cap = options.maxBytes ?? MAX_BYTES;
|
|
65
|
+
if (Number.isFinite(declared) && declared > cap) {
|
|
66
|
+
throw new FetchProblem(`${target} declared ${declared} bytes`);
|
|
67
|
+
}
|
|
68
|
+
const text = await readCapped(response, cap);
|
|
69
|
+
try {
|
|
70
|
+
return JSON.parse(text);
|
|
71
|
+
}
|
|
72
|
+
catch {
|
|
73
|
+
throw new FetchProblem(`${target} did not answer with JSON`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
throw new FetchProblem(`${url} redirected too many times`);
|
|
77
|
+
}
|
|
78
|
+
catch (error) {
|
|
79
|
+
if (error instanceof FetchProblem)
|
|
80
|
+
throw error;
|
|
81
|
+
if (error instanceof Error && error.name === 'AbortError') {
|
|
82
|
+
throw new FetchProblem(`${url} timed out`);
|
|
83
|
+
}
|
|
84
|
+
throw new FetchProblem(`${url}: ${error instanceof Error ? error.message : String(error)}`);
|
|
85
|
+
}
|
|
86
|
+
finally {
|
|
87
|
+
clearTimeout(timer);
|
|
88
|
+
options.signal?.removeEventListener('abort', abort);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function readCapped(response, cap) {
|
|
92
|
+
const body = response.body;
|
|
93
|
+
if (body === null)
|
|
94
|
+
return '';
|
|
95
|
+
const reader = body.getReader();
|
|
96
|
+
const chunks = [];
|
|
97
|
+
let total = 0;
|
|
98
|
+
for (;;) {
|
|
99
|
+
const { done, value } = await reader.read();
|
|
100
|
+
if (done)
|
|
101
|
+
break;
|
|
102
|
+
if (value === undefined)
|
|
103
|
+
continue;
|
|
104
|
+
total += value.byteLength;
|
|
105
|
+
if (total > cap) {
|
|
106
|
+
await reader.cancel();
|
|
107
|
+
throw new FetchProblem(`response exceeded ${cap} bytes`);
|
|
108
|
+
}
|
|
109
|
+
chunks.push(value);
|
|
110
|
+
}
|
|
111
|
+
const joined = new Uint8Array(total);
|
|
112
|
+
let offset = 0;
|
|
113
|
+
for (const chunk of chunks) {
|
|
114
|
+
joined.set(chunk, offset);
|
|
115
|
+
offset += chunk.byteLength;
|
|
116
|
+
}
|
|
117
|
+
return new TextDecoder().decode(joined);
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Read an instance's descriptor from the instance itself.
|
|
121
|
+
*
|
|
122
|
+
* The URL it is parsed against is where we fetched it, never what the document
|
|
123
|
+
* claims, so an instance cannot announce itself as somebody else.
|
|
124
|
+
*/
|
|
125
|
+
export async function fetchDescriptor(origin, options = {}) {
|
|
126
|
+
const base = publishable(origin);
|
|
127
|
+
if (base === null)
|
|
128
|
+
throw new FetchProblem(`${origin} is not an address we will list`);
|
|
129
|
+
const payload = await fetchJson(`${base.origin}${WELL_KNOWN_PATH}`, {
|
|
130
|
+
...options,
|
|
131
|
+
maxBytes: 64 * 1024,
|
|
132
|
+
});
|
|
133
|
+
const descriptor = parseDescriptor(payload, base.origin);
|
|
134
|
+
if (descriptor === null)
|
|
135
|
+
throw new FetchProblem(`${base.origin} did not serve a usable descriptor`);
|
|
136
|
+
return descriptor;
|
|
137
|
+
}
|
|
138
|
+
//# sourceMappingURL=fetch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fetch.js","sourceRoot":"","sources":["../../src/directory/fetch.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/directory/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC;AAC9B,cAAc,eAAe,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The directory: which instances exist.
|
|
3
|
+
*
|
|
4
|
+
* An instance announces itself by sending nothing but its own URL. The
|
|
5
|
+
* directory then fetches that URL's descriptor itself and records what it
|
|
6
|
+
* actually read. nixamp's stream directory trusts the announcement body, which
|
|
7
|
+
* is right for a stream whose only claim is "I am playing something"; here an
|
|
8
|
+
* announcement carries a name, a topic list and a job count, and a board that
|
|
9
|
+
* could announce someone else's name with its own URL underneath would be a
|
|
10
|
+
* phishing primitive on day one.
|
|
11
|
+
*/
|
|
12
|
+
import type pg from 'pg';
|
|
13
|
+
import { type InstanceListing } from '../schema/instance.ts';
|
|
14
|
+
export declare class Blocked extends Error {
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Record an announcement.
|
|
18
|
+
*
|
|
19
|
+
* Idempotent per origin: an instance that heartbeats every ten minutes updates
|
|
20
|
+
* one row forever rather than accumulating a history of itself.
|
|
21
|
+
*/
|
|
22
|
+
export declare function announce(pool: pg.Pool, rawUrl: string): Promise<InstanceListing>;
|
|
23
|
+
export interface ListOptions {
|
|
24
|
+
topic?: string | null;
|
|
25
|
+
q?: string | null;
|
|
26
|
+
onlineOnly?: boolean;
|
|
27
|
+
limit?: number;
|
|
28
|
+
offset?: number;
|
|
29
|
+
}
|
|
30
|
+
export declare function listInstances(pool: pg.Pool, options?: ListOptions): Promise<InstanceListing[]>;
|
|
31
|
+
export declare function getInstance(pool: pg.Pool, url: string): Promise<InstanceListing | null>;
|
|
32
|
+
/** Every topic the directory knows about, most-used first. */
|
|
33
|
+
export declare function listTopics(pool: pg.Pool): Promise<{
|
|
34
|
+
topic: string;
|
|
35
|
+
instances: number;
|
|
36
|
+
}[]>;
|
|
37
|
+
export interface SweepResult {
|
|
38
|
+
checked: number;
|
|
39
|
+
ok: number;
|
|
40
|
+
failed: number;
|
|
41
|
+
dropped: number;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Re-read every instance the directory knows about.
|
|
45
|
+
*
|
|
46
|
+
* A failure raises a counter rather than deleting the row: an instance is
|
|
47
|
+
* allowed to be down for a redeploy without losing its place, and the URL only
|
|
48
|
+
* goes away after MAX_FAILURES consecutive misses, which at the sweep interval
|
|
49
|
+
* is the better part of a day.
|
|
50
|
+
*/
|
|
51
|
+
export declare function sweep(pool: pg.Pool, options?: {
|
|
52
|
+
limit?: number;
|
|
53
|
+
concurrency?: number;
|
|
54
|
+
}): Promise<SweepResult>;
|
|
55
|
+
export declare function blockInstance(pool: pg.Pool, url: string, reason: string): Promise<boolean>;
|