@agentage/extension 0.1.1 → 0.2.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 (143) hide show
  1. package/README.md +6 -5
  2. package/dist/agcid/agcid.d.ts +1 -0
  3. package/dist/agcid/agcid.js +2 -2
  4. package/dist/agcid/current.d.ts +7 -0
  5. package/dist/agcid/current.js +4 -0
  6. package/dist/agcid/scope.d.ts +1 -1
  7. package/dist/agcid/scope.js +3 -4
  8. package/dist/command/args.d.ts +14 -0
  9. package/dist/command/args.js +60 -0
  10. package/dist/command/check-cli.d.ts +1 -0
  11. package/dist/command/check-cli.js +24 -0
  12. package/dist/command/command.types.d.ts +14 -0
  13. package/dist/command/command.types.js +2 -0
  14. package/dist/command/out.d.ts +19 -0
  15. package/dist/command/out.js +32 -0
  16. package/dist/command/run-command.d.ts +15 -0
  17. package/dist/command/run-command.js +53 -0
  18. package/dist/context/context.d.ts +8 -14
  19. package/dist/context/context.js +19 -21
  20. package/dist/context/settings.d.ts +5 -0
  21. package/dist/context/settings.js +32 -0
  22. package/dist/context/test-context.d.ts +44 -0
  23. package/dist/context/test-context.js +72 -0
  24. package/dist/discovery/discovery.d.ts +12 -1
  25. package/dist/discovery/discovery.js +21 -27
  26. package/dist/discovery/discovery.schema.d.ts +2 -26
  27. package/dist/discovery/discovery.schema.js +6 -4
  28. package/dist/discovery/openapi.d.ts +3 -0
  29. package/dist/discovery/openapi.js +19 -0
  30. package/dist/envelope/envelope.js +1 -0
  31. package/dist/envelope/envelope.schema.d.ts +0 -4
  32. package/dist/envelope/envelope.schema.js +3 -2
  33. package/dist/health/health.d.ts +5 -2
  34. package/dist/health/health.js +23 -8
  35. package/dist/health/health.schema.d.ts +2 -9
  36. package/dist/health/health.schema.js +3 -3
  37. package/dist/index.d.ts +19 -12
  38. package/dist/index.js +19 -12
  39. package/dist/log/log.d.ts +1 -0
  40. package/dist/log/log.js +7 -8
  41. package/dist/manifest/manifest.schema.d.ts +5 -75
  42. package/dist/manifest/manifest.schema.js +8 -4
  43. package/dist/manifest/primitives.schema.d.ts +1 -0
  44. package/dist/manifest/primitives.schema.js +3 -1
  45. package/dist/manifest/read-manifest.d.ts +1 -0
  46. package/dist/manifest/read-manifest.js +26 -3
  47. package/dist/mcp/mcp-app.d.ts +8 -0
  48. package/dist/mcp/mcp-app.js +74 -0
  49. package/dist/mcp/server.d.ts +8 -0
  50. package/dist/mcp/server.js +36 -0
  51. package/dist/mcp/snapshot.d.ts +11 -0
  52. package/dist/mcp/snapshot.js +69 -0
  53. package/dist/mcp/tools.d.ts +24 -0
  54. package/dist/mcp/tools.js +17 -0
  55. package/dist/pages/pages-app.d.ts +8 -0
  56. package/dist/pages/pages-app.js +29 -0
  57. package/dist/pages/pages.d.ts +4 -0
  58. package/dist/pages/pages.js +16 -0
  59. package/dist/runtime/build-info.d.ts +1 -0
  60. package/dist/runtime/build-info.js +4 -4
  61. package/dist/runtime/main.js +5 -2
  62. package/dist/runtime/part-runners.d.ts +20 -0
  63. package/dist/runtime/part-runners.js +68 -0
  64. package/dist/runtime/run-part.d.ts +4 -2
  65. package/dist/runtime/run-part.js +47 -34
  66. package/dist/server/app.d.ts +16 -2
  67. package/dist/server/app.js +28 -12
  68. package/dist/server/errors.d.ts +0 -2
  69. package/dist/server/errors.js +1 -1
  70. package/dist/server/serve.d.ts +0 -1
  71. package/dist/server/serve.js +2 -1
  72. package/dist/store/migrate.d.ts +2 -0
  73. package/dist/store/migrate.js +18 -0
  74. package/dist/store/migrations.d.ts +10 -0
  75. package/dist/store/migrations.js +55 -0
  76. package/dist/store/store.d.ts +10 -0
  77. package/dist/store/store.js +25 -0
  78. package/dist/store/store.types.d.ts +7 -0
  79. package/dist/store/store.types.js +1 -0
  80. package/dist/store/test-store.d.ts +5 -0
  81. package/dist/store/test-store.js +39 -0
  82. package/dist/testing/test-server.d.ts +6 -0
  83. package/dist/testing/test-server.js +31 -9
  84. package/dist/tool/build-info.js +2 -3
  85. package/dist/tool/build.d.ts +5 -2
  86. package/dist/tool/build.js +16 -24
  87. package/dist/tool/docker.d.ts +0 -1
  88. package/dist/tool/docker.js +2 -1
  89. package/dist/tool/exec.d.ts +4 -1
  90. package/dist/tool/exec.js +13 -5
  91. package/dist/tool/index.d.ts +9 -10
  92. package/dist/tool/index.js +9 -10
  93. package/dist/tool/kit-version.d.ts +1 -0
  94. package/dist/tool/kit-version.js +3 -0
  95. package/dist/tool/main.d.ts +6 -1
  96. package/dist/tool/main.js +58 -30
  97. package/dist/tool/recipe.d.ts +0 -1
  98. package/dist/tool/recipe.js +7 -3
  99. package/dist/tool/record-extras.d.ts +4 -0
  100. package/dist/tool/record-extras.js +60 -0
  101. package/dist/tool/stage.d.ts +3 -0
  102. package/dist/tool/stage.js +45 -12
  103. package/dist/tool/up/compose-run.d.ts +14 -0
  104. package/dist/tool/up/compose-run.js +24 -0
  105. package/dist/tool/up/compose.d.ts +18 -0
  106. package/dist/tool/up/compose.js +77 -0
  107. package/dist/tool/up/dev-issuer-app.d.ts +22 -0
  108. package/dist/tool/up/dev-issuer-app.js +80 -0
  109. package/dist/tool/up/dev-issuer.d.ts +2 -0
  110. package/dist/tool/up/dev-issuer.js +5 -0
  111. package/dist/tool/up/down.d.ts +12 -0
  112. package/dist/tool/up/down.js +38 -0
  113. package/dist/tool/up/gateway.d.ts +27 -0
  114. package/dist/tool/up/gateway.js +66 -0
  115. package/dist/tool/up/lines.d.ts +2 -0
  116. package/dist/tool/up/lines.js +3 -0
  117. package/dist/tool/up/local-record.d.ts +44 -0
  118. package/dist/tool/up/local-record.js +28 -0
  119. package/dist/tool/up/token.d.ts +6 -0
  120. package/dist/tool/up/token.js +18 -0
  121. package/dist/tool/up/up.d.ts +19 -0
  122. package/dist/tool/up/up.js +81 -0
  123. package/dist/tool/up/wait.d.ts +8 -0
  124. package/dist/tool/up/wait.js +24 -0
  125. package/dist/ui/ui-app.d.ts +6 -0
  126. package/dist/ui/ui-app.js +26 -0
  127. package/dist/worker/cron.d.ts +11 -0
  128. package/dist/worker/cron.js +73 -0
  129. package/dist/worker/job-options.d.ts +9 -0
  130. package/dist/worker/job-options.js +17 -0
  131. package/dist/worker/run-log.d.ts +9 -0
  132. package/dist/worker/run-log.js +38 -0
  133. package/dist/worker/runs.d.ts +20 -0
  134. package/dist/worker/runs.js +59 -0
  135. package/dist/worker/scheduler.d.ts +20 -0
  136. package/dist/worker/scheduler.js +97 -0
  137. package/dist/worker/worker.types.d.ts +5 -0
  138. package/dist/worker/worker.types.js +2 -0
  139. package/package.json +11 -2
  140. package/schemas/extension-discovery.schema.json +5 -0
  141. package/schemas/extension-package.schema.json +3 -0
  142. package/schemas/extension-record.schema.json +5 -0
  143. package/schemas/health.schema.json +1 -1
@@ -0,0 +1,66 @@
1
+ import {} from '../../manifest/primitives.schema.js';
2
+ export const GATEWAY_PORT = 4280;
3
+ export const ISSUER_PORT = 4281;
4
+ export const PART_PORT = 3000;
5
+ export const TRAEFIK_IMAGE = 'traefik:v3.7.17@sha256:68ae5fe5b64ca166704b746a9e8afcd385e36b3f569a5691401bf652d57b3748';
6
+ // The URL layout of api-standards section 1, as the estate's gateway renders it.
7
+ const RESERVED = 'PathPrefix(`/api/`) || Path(`/mcp`) || PathPrefix(`/mcp/`) || PathPrefix(`/ui/`) || PathPrefix(`/.well-known/`) || PathPrefix(`/git/`) || Path(`/health`)';
8
+ const ROUTED = ['api', 'mcp', 'pages', 'ui'];
9
+ const ruleOf = (part, version) => ({
10
+ api: 'PathPrefix(`/api/v1/`)',
11
+ mcp: 'Path(`/mcp`)',
12
+ ui: `PathPrefix(\`/ui/${version}/\`)`,
13
+ pages: 'PathPrefix(`/`)',
14
+ worker: undefined,
15
+ })[part];
16
+ const router = (rule, priority, service) => ({
17
+ rule,
18
+ priority,
19
+ service,
20
+ entryPoints: ['web'],
21
+ middlewares: ['gateway-headers'],
22
+ });
23
+ const partRouters = (parts, version) => {
24
+ const routers = {};
25
+ for (const part of parts) {
26
+ const rule = ruleOf(part, version);
27
+ if (rule)
28
+ routers[part] = router(rule, part === 'pages' ? 1 : 100, part);
29
+ }
30
+ const owner = ROUTED.find((part) => parts.includes(part));
31
+ if (owner)
32
+ routers['well-known'] = router('PathPrefix(`/.well-known/`)', 100, owner);
33
+ return routers;
34
+ };
35
+ // Traefik's file provider: a reserved-prefix miss is Traefik's own 404, like the estate gateway.
36
+ export const gatewayConfig = (parts, version) => {
37
+ const routed = parts.filter((part) => ROUTED.includes(part));
38
+ const services = {
39
+ 'gateway-miss': { loadBalancer: { servers: [{ url: 'http://127.0.0.1:8081' }] } },
40
+ };
41
+ for (const part of routed)
42
+ services[part] = { loadBalancer: { servers: [{ url: `http://${part}:${PART_PORT}` }] } };
43
+ const routers = {
44
+ reserved: router(RESERVED, 50, 'gateway-miss'),
45
+ ...partRouters(routed, version),
46
+ };
47
+ if (!routed.includes('pages'))
48
+ routers['host'] = router('PathPrefix(`/`)', 1, 'gateway-miss');
49
+ const headers = {
50
+ contentTypeNosniff: true,
51
+ referrerPolicy: 'strict-origin-when-cross-origin',
52
+ frameDeny: true,
53
+ };
54
+ return { http: { middlewares: { 'gateway-headers': { headers } }, services, routers } };
55
+ };
56
+ export const TRAEFIK_COMMAND = [
57
+ '--global.checkNewVersion=false',
58
+ '--global.sendAnonymousUsage=false',
59
+ '--log.format=json',
60
+ '--providers.file.filename=/etc/traefik/dynamic.yml',
61
+ '--entryPoints.web.address=:80',
62
+ // No routers listen here, so every request it receives gets Traefik's own 404.
63
+ '--entryPoints.miss.address=:8081',
64
+ '--entryPoints.ping.address=127.0.0.1:8082',
65
+ '--ping.entryPoint=ping',
66
+ ];
@@ -0,0 +1,2 @@
1
+ export declare const stepLine: (label: string, what: string, detail?: string) => string;
2
+ export declare const plural: (n: number, word: string) => string;
@@ -0,0 +1,3 @@
1
+ // What `up` and `down` print: a label, what it is, the detail; the columns line up as in a terminal.
2
+ export const stepLine = (label, what, detail = '') => `${label.padEnd(9)}${what.padEnd(45)}${detail}`.trimEnd();
3
+ export const plural = (n, word) => `${n} ${word}${n === 1 ? '' : 's'}`;
@@ -0,0 +1,44 @@
1
+ import { type ExtensionRecord } from '../../discovery/discovery.schema.js';
2
+ import { type ExtensionPackage } from '../../manifest/manifest.schema.js';
3
+ import { type BuildResult } from '../build.js';
4
+ import { type Run } from '../exec.js';
5
+ export declare const LOCAL_DIR = ".agentage";
6
+ export declare const assertDiscoverable: (record: ExtensionRecord, origin: string) => void;
7
+ export declare const writeLocalRecord: (dir: string, pkg: ExtensionPackage, built: BuildResult, run: Run) => Promise<{
8
+ file: string;
9
+ record: {
10
+ id: string;
11
+ version: string;
12
+ commit: string;
13
+ buildTime: string;
14
+ parts: {
15
+ pages?: {
16
+ image: string;
17
+ } | undefined;
18
+ ui?: {
19
+ image: string;
20
+ } | undefined;
21
+ api?: {
22
+ image: string;
23
+ } | undefined;
24
+ mcp?: {
25
+ image: string;
26
+ } | undefined;
27
+ worker?: {
28
+ image: string;
29
+ } | undefined;
30
+ store?: {
31
+ kind: "postgres" | "git" | "signoz";
32
+ migrations?: number | undefined;
33
+ } | undefined;
34
+ };
35
+ openapi?: string | undefined;
36
+ tools?: string[] | undefined;
37
+ widgets?: {
38
+ name: string;
39
+ element: string;
40
+ module: string;
41
+ }[] | undefined;
42
+ commands?: string[] | undefined;
43
+ };
44
+ }>;
@@ -0,0 +1,28 @@
1
+ import { mkdir, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { discoveryDocument } from '../../discovery/discovery.js';
4
+ import { DiscoveryDocumentSchema } from '../../discovery/discovery.schema.js';
5
+ import { formatIssues } from '../../manifest/read-manifest.js';
6
+ import {} from '../../manifest/manifest.schema.js';
7
+ import { recordFor } from '../build.js';
8
+ import { mustRun } from '../exec.js';
9
+ export const LOCAL_DIR = '.agentage';
10
+ // A local build has no registry digest; the image id from the daemon stands in for it (Q4).
11
+ const localImage = async (run, built) => {
12
+ const id = (await mustRun(run, 'docker', ['image', 'inspect', '-f', '{{.Id}}', built.tag])).trim();
13
+ const repository = built.tag.slice(0, built.tag.lastIndexOf(':'));
14
+ return { ...built, digest: id, image: `${repository}@${id}` };
15
+ };
16
+ export const assertDiscoverable = (record, origin) => {
17
+ const result = DiscoveryDocumentSchema.safeParse(discoveryDocument(record, origin));
18
+ if (!result.success)
19
+ throw new Error(`discovery would be invalid: ${formatIssues(result.error.issues)}`);
20
+ };
21
+ export const writeLocalRecord = async (dir, pkg, built, run) => {
22
+ const images = await Promise.all(built.images.map((image) => localImage(run, image)));
23
+ const record = await recordFor(dir, pkg, built, images);
24
+ await mkdir(join(dir, LOCAL_DIR), { recursive: true });
25
+ const file = join(dir, LOCAL_DIR, 'record.json');
26
+ await writeFile(file, `${JSON.stringify(record, null, 2)}\n`);
27
+ return { file, record };
28
+ };
@@ -0,0 +1,6 @@
1
+ export interface TokenOptions {
2
+ scope: string;
3
+ issuer?: string;
4
+ fetch?: typeof fetch;
5
+ }
6
+ export declare const token: (options: TokenOptions) => Promise<string>;
@@ -0,0 +1,18 @@
1
+ import { z } from 'zod';
2
+ import { DEV_ISSUER } from './dev-issuer-app.js';
3
+ const AnswerSchema = z.object({ token: z.string().startsWith('aga_') });
4
+ // A token from the dev issuer of `up`, for calling /api/v1/ and /mcp by hand or from tests.
5
+ export const token = async (options) => {
6
+ const issuer = options.issuer ?? DEV_ISSUER;
7
+ const res = await (options.fetch ?? fetch)(`${issuer}/token`, {
8
+ method: 'POST',
9
+ headers: { 'content-type': 'application/json' },
10
+ body: JSON.stringify({ scope: options.scope }),
11
+ }).catch(() => {
12
+ throw new Error(`no dev issuer at ${issuer}: run agentage extension up first`);
13
+ });
14
+ const body = await res.json().catch(() => null);
15
+ if (!res.ok)
16
+ throw new Error(`the dev issuer refused: ${JSON.stringify(body)}`);
17
+ return AnswerSchema.parse(body).token;
18
+ };
@@ -0,0 +1,19 @@
1
+ import { type ExtensionPackage } from '../../manifest/manifest.schema.js';
2
+ import { type Env } from '../../runtime/build-info.js';
3
+ import { type Run } from '../exec.js';
4
+ export interface UpOptions {
5
+ dir?: string;
6
+ env?: Env;
7
+ onLine?: (line: string) => void;
8
+ }
9
+ export interface UpDeps {
10
+ run: Run;
11
+ sleep?: (ms: number) => Promise<void>;
12
+ timeoutMs?: number;
13
+ }
14
+ export interface UpResult {
15
+ parts: number;
16
+ seconds: number;
17
+ }
18
+ export declare const routesOf: (pkg: ExtensionPackage) => string[];
19
+ export declare const up: (options?: UpOptions, deps?: UpDeps) => Promise<UpResult>;
@@ -0,0 +1,81 @@
1
+ import { writeFile } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { checkCli } from '../../command/check-cli.js';
4
+ import {} from '../../manifest/manifest.schema.js';
5
+ import { imageParts, normalizeParts } from '../../manifest/parts.js';
6
+ import { readManifest } from '../../manifest/read-manifest.js';
7
+ import {} from '../../runtime/build-info.js';
8
+ import { build } from '../build.js';
9
+ import { quietly, runCommand } from '../exec.js';
10
+ import { compose } from './compose-run.js';
11
+ import { composeFile, projectName, STORE_IMAGE } from './compose.js';
12
+ import { GATEWAY_PORT, gatewayConfig, ISSUER_PORT } from './gateway.js';
13
+ import { plural, stepLine } from './lines.js';
14
+ import { assertDiscoverable, LOCAL_DIR, writeLocalRecord } from './local-record.js';
15
+ import { waitHealthy } from './wait.js';
16
+ const GATEWAY = `http://localhost:${GATEWAY_PORT}`;
17
+ const renderFiles = async (s, recordFile, tags) => {
18
+ const gatewayFile = join(s.dir, LOCAL_DIR, 'gateway.yml');
19
+ await writeFile(gatewayFile, `${JSON.stringify(gatewayConfig(imageParts(s.pkg), s.pkg.version), null, 2)}\n`);
20
+ await writeFile(s.target.file ?? '', `${JSON.stringify(composeFile({ pkg: s.pkg, tags, recordFile, gatewayFile }), null, 2)}\n`);
21
+ };
22
+ const buildStep = async (s, env) => {
23
+ const built = await build({ dir: s.dir, env }, { run: s.run });
24
+ s.line('build', `${plural(built.images.length, 'image')} from the release recipes`, 'same recipe and tags as a release');
25
+ const { file, record } = await writeLocalRecord(s.dir, s.pkg, built, s.run);
26
+ assertDiscoverable(record, GATEWAY);
27
+ s.line('record', `${LOCAL_DIR}/record.json`, 'discovery valid');
28
+ await renderFiles(s, file, Object.fromEntries(built.images.map((image) => [image.part, image.tag])));
29
+ };
30
+ // The migrate job runs before any part starts; its last stdout line is the summary.
31
+ const storeStep = async (s) => {
32
+ await compose(s.run, s.target, ['up', '-d', 'store']);
33
+ await waitHealthy(s.target, ['store'], s.deps);
34
+ const out = await compose(s.run, s.target, ['run', '--rm', '--no-deps', 'migrate']);
35
+ const summary = JSON.parse(out.trim().split('\n').at(-1) ?? '{}');
36
+ s.line('store', STORE_IMAGE, `${plural(summary.applied ?? 0, 'migration')} applied · ${plural(summary.seeds ?? 0, 'seed')}`);
37
+ };
38
+ const ROUTES = { pages: '/', ui: '/ui/{version}/', api: '/api/v1/', mcp: '/mcp' };
39
+ export const routesOf = (pkg) => Object.keys(ROUTES)
40
+ .filter((part) => imageParts(pkg).includes(part))
41
+ .map((part) => ROUTES[part].replace('{version}', pkg.version));
42
+ const partsStep = async (s) => {
43
+ await compose(s.run, s.target, ['up', '-d', '--remove-orphans']);
44
+ const services = [...imageParts(s.pkg), 'gateway', 'issuer'];
45
+ await waitHealthy(s.target, services, s.deps);
46
+ s.line('issuer', 'dev tokens', `http://localhost:${ISSUER_PORT}`);
47
+ s.line('gateway', GATEWAY, routesOf(s.pkg).join(' · '));
48
+ };
49
+ const cliStep = async (s) => {
50
+ if (!normalizeParts(s.pkg.agentage.parts).code.cli)
51
+ return 0;
52
+ const problem = await checkCli(s.dir);
53
+ if (problem)
54
+ throw new Error(`cli part: ${problem}`);
55
+ return 1;
56
+ };
57
+ // The laptop shape of a deployment: the release images, a local record, store, issuer, gateway.
58
+ export const up = async (options = {}, deps = { run: runCommand }) => {
59
+ const started = Date.now();
60
+ const dir = resolve(options.dir ?? '.');
61
+ const pkg = await readManifest(dir);
62
+ const target = { file: join(dir, LOCAL_DIR, 'compose.json'), project: projectName(pkg) };
63
+ const line = (label, what, detail) => options.onLine?.(stepLine(label, what, detail));
64
+ const s = {
65
+ dir,
66
+ pkg,
67
+ run: quietly(deps.run),
68
+ target,
69
+ line,
70
+ deps: { ...deps, run: quietly(deps.run) },
71
+ };
72
+ await buildStep(s, options.env ?? process.env);
73
+ const store = normalizeParts(pkg.agentage.parts).store === 'postgres';
74
+ if (store)
75
+ await storeStep(s);
76
+ await partsStep(s);
77
+ const parts = imageParts(pkg).length + (store ? 1 : 0) + (await cliStep(s));
78
+ const seconds = Math.round((Date.now() - started) / 1000);
79
+ line('ready', `${plural(parts, 'part')} healthy`, `${seconds} s`);
80
+ return { parts, seconds };
81
+ };
@@ -0,0 +1,8 @@
1
+ import { type Run } from '../exec.js';
2
+ import { type ComposeTarget } from './compose-run.js';
3
+ export interface WaitDeps {
4
+ run: Run;
5
+ sleep?: (ms: number) => Promise<void>;
6
+ timeoutMs?: number;
7
+ }
8
+ export declare const waitHealthy: (target: ComposeTarget, services: string[], deps: WaitDeps) => Promise<void>;
@@ -0,0 +1,24 @@
1
+ import {} from '../exec.js';
2
+ import { compose, states } from './compose-run.js';
3
+ const pause = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
4
+ const broken = (state) => state !== undefined &&
5
+ (state.health === 'unhealthy' || state.state === 'exited' || state.state === 'dead');
6
+ const logsOf = async (run, target, service) => compose(run, target, ['logs', '--no-color', '--tail', '20', service]).catch(() => '');
7
+ const failure = async (run, target, state, why) => new Error(`${state.service} ${why}\n${(await logsOf(run, target, state.service)).trim()}`);
8
+ // Every named service healthy (its HEALTHCHECK on 127.0.0.1), or the first broken one with its log.
9
+ export const waitHealthy = async (target, services, deps) => {
10
+ const deadline = Date.now() + (deps.timeoutMs ?? 180_000);
11
+ for (;;) {
12
+ const byService = new Map((await states(deps.run, target)).map((s) => [s.service, s]));
13
+ const bad = services.map((name) => byService.get(name)).find(broken);
14
+ if (bad)
15
+ throw await failure(deps.run, target, bad, `is ${bad.health || bad.state}`);
16
+ if (services.every((name) => byService.get(name)?.health === 'healthy'))
17
+ return;
18
+ if (Date.now() > deadline) {
19
+ const waiting = services.filter((name) => byService.get(name)?.health !== 'healthy');
20
+ throw new Error(`not healthy after ${Math.round((deps.timeoutMs ?? 180_000) / 1000)} s: ${waiting.join(', ')}`);
21
+ }
22
+ await (deps.sleep ?? pause)(1000);
23
+ }
24
+ };
@@ -0,0 +1,6 @@
1
+ import { type Express } from 'express';
2
+ import { type PartAppBase } from '../server/app.js';
3
+ export interface UiAppOptions extends PartAppBase {
4
+ dir: string;
5
+ }
6
+ export declare const createUiApp: (options: UiAppOptions) => Express;
@@ -0,0 +1,26 @@
1
+ import cors from 'cors';
2
+ import express, {} from 'express';
3
+ import { createPartApp, PUBLIC_CORS } from '../server/app.js';
4
+ // Widgets reach their own /mcp and /api only; the dev issuer of `up` is the one other origin.
5
+ const CSP = {
6
+ directives: {
7
+ defaultSrc: ["'self'"],
8
+ scriptSrc: ["'self'"],
9
+ styleSrc: ["'self'", "'unsafe-inline'"],
10
+ connectSrc: ["'self'", 'http://localhost:4281', 'http://127.0.0.1:4281'],
11
+ imgSrc: ["'self'", 'data:'],
12
+ upgradeInsecureRequests: null,
13
+ },
14
+ };
15
+ const IMMUTABLE = 'public, max-age=31536000, immutable';
16
+ // /ui/{version}/: static widget modules, anonymous, cached forever (a version never changes).
17
+ export const createUiApp = (options) => createPartApp({
18
+ ...options,
19
+ part: 'ui',
20
+ helmet: { contentSecurityPolicy: CSP, crossOriginResourcePolicy: { policy: 'cross-origin' } },
21
+ mount: (app) => void app.use(`/ui/${options.pkg.version}`, cors(PUBLIC_CORS), express.static(options.dir, {
22
+ index: false,
23
+ fallthrough: true,
24
+ setHeaders: (res) => res.setHeader('Cache-Control', IMMUTABLE),
25
+ })),
26
+ });
@@ -0,0 +1,11 @@
1
+ export interface Cron {
2
+ readonly expression: string;
3
+ readonly sets: readonly ReadonlySet<number>[];
4
+ readonly anyDay: {
5
+ dom: boolean;
6
+ dow: boolean;
7
+ };
8
+ }
9
+ export declare const parseCron: (expression: string) => Cron;
10
+ export declare const matches: (cron: Cron, date: Date) => boolean;
11
+ export declare const nextRun: (cron: Cron, after: Date) => Date;
@@ -0,0 +1,73 @@
1
+ // Five fields: minute hour day-of-month month day-of-week (0 or 7 is Sunday), in UTC.
2
+ const FIELDS = [
3
+ { name: 'minute', min: 0, max: 59 },
4
+ { name: 'hour', min: 0, max: 23 },
5
+ { name: 'day of month', min: 1, max: 31 },
6
+ { name: 'month', min: 1, max: 12 },
7
+ { name: 'day of week', min: 0, max: 7 },
8
+ ];
9
+ const toInt = (text, field) => {
10
+ const value = Number(text);
11
+ if (!/^[0-9]+$/.test(text) || value < field.min || value > field.max) {
12
+ throw new Error(`cron ${field.name}: ${text} is not ${field.min}-${field.max}`);
13
+ }
14
+ return value;
15
+ };
16
+ const range = (part, field) => {
17
+ if (part === '*')
18
+ return [field.min, field.max];
19
+ const [from = '', to] = part.split('-');
20
+ return [toInt(from, field), to === undefined ? toInt(from, field) : toInt(to, field)];
21
+ };
22
+ const expand = (part, field) => {
23
+ const [base = '', stepText] = part.split('/');
24
+ const step = stepText === undefined ? 1 : Number(stepText);
25
+ if (!Number.isInteger(step) || step < 1)
26
+ throw new Error(`cron ${field.name}: bad step in ${part}`);
27
+ const [from, to] = range(base, field);
28
+ if (from > to)
29
+ throw new Error(`cron ${field.name}: ${part} runs backwards`);
30
+ const values = [];
31
+ for (let v = from; v <= to; v += step)
32
+ values.push(v);
33
+ return values;
34
+ };
35
+ const fieldSet = (text, field) => {
36
+ const values = text.split(',').flatMap((part) => expand(part, field));
37
+ return new Set(field.name === 'day of week' ? values.map((v) => v % 7) : values);
38
+ };
39
+ export const parseCron = (expression) => {
40
+ const parts = expression.trim().split(/\s+/);
41
+ if (parts.length !== 5)
42
+ throw new Error(`cron "${expression}": five fields expected`);
43
+ const sets = FIELDS.map((field, i) => fieldSet(parts[i] ?? '', field));
44
+ return { expression, sets, anyDay: { dom: parts[2] === '*', dow: parts[4] === '*' } };
45
+ };
46
+ // Classic cron: with both day fields restricted, either one matching is enough.
47
+ const dayMatches = (cron, date) => {
48
+ const dom = cron.sets[2]?.has(date.getUTCDate()) ?? false;
49
+ const dow = cron.sets[4]?.has(date.getUTCDay()) ?? false;
50
+ if (cron.anyDay.dom && cron.anyDay.dow)
51
+ return true;
52
+ if (cron.anyDay.dom)
53
+ return dow;
54
+ if (cron.anyDay.dow)
55
+ return dom;
56
+ return dom || dow;
57
+ };
58
+ export const matches = (cron, date) => (cron.sets[0]?.has(date.getUTCMinutes()) ?? false) &&
59
+ (cron.sets[1]?.has(date.getUTCHours()) ?? false) &&
60
+ (cron.sets[3]?.has(date.getUTCMonth() + 1) ?? false) &&
61
+ dayMatches(cron, date);
62
+ const MINUTE = 60_000;
63
+ const HORIZON_MINUTES = 5 * 366 * 24 * 60;
64
+ // The first matching minute strictly after `after`.
65
+ export const nextRun = (cron, after) => {
66
+ let t = Math.floor(after.getTime() / MINUTE) * MINUTE + MINUTE;
67
+ for (let i = 0; i < HORIZON_MINUTES; i += 1, t += MINUTE) {
68
+ const date = new Date(t);
69
+ if (matches(cron, date))
70
+ return date;
71
+ }
72
+ throw new Error(`cron "${cron.expression}" never runs`);
73
+ };
@@ -0,0 +1,9 @@
1
+ import { z } from 'zod';
2
+ export declare const JobNameSchema: z.ZodString;
3
+ export declare const JobOptionsSchema: z.ZodOptional<z.ZodObject<{
4
+ timeout: z.ZodOptional<z.ZodUnion<readonly [z.ZodInt, z.ZodString]>>;
5
+ }, z.core.$strict>>;
6
+ export interface JobOptions {
7
+ timeout?: number | string;
8
+ }
9
+ export declare const toMs: (duration: number | string) => number;
@@ -0,0 +1,17 @@
1
+ import { z } from 'zod';
2
+ const UNIT_MS = { ms: 1, s: 1000, m: 60_000, h: 3_600_000 };
3
+ const DurationSchema = z.union([
4
+ z.int().positive(),
5
+ z.string().regex(/^[0-9]+(ms|s|m|h)$/, { message: 'a duration like 500ms, 30s, 5m or 1h' }),
6
+ ]);
7
+ export const JobNameSchema = z
8
+ .string()
9
+ .max(48)
10
+ .regex(/^[a-z][a-z0-9-]*$/, { message: 'job names are lowercase, digits and dashes' });
11
+ export const JobOptionsSchema = z.strictObject({ timeout: DurationSchema.optional() }).optional();
12
+ export const toMs = (duration) => {
13
+ if (typeof duration === 'number')
14
+ return duration;
15
+ const match = /^([0-9]+)(ms|s|m|h)$/.exec(duration);
16
+ return Number(match?.[1]) * UNIT_MS[(match?.[2] ?? 'ms')];
17
+ };
@@ -0,0 +1,9 @@
1
+ import { type Log } from '../log/log.js';
2
+ export declare const LOG_CAP_BYTES: number;
3
+ export interface RunBuffer {
4
+ text: string;
5
+ truncated: boolean;
6
+ }
7
+ export declare const withRunBuffer: <T>(buffer: RunBuffer, fn: () => Promise<T>) => Promise<T>;
8
+ export declare const append: (buffer: RunBuffer, line: string) => void;
9
+ export declare const runLog: (base: Log) => Log;
@@ -0,0 +1,38 @@
1
+ import { AsyncLocalStorage } from 'node:async_hooks';
2
+ import { messageOf } from '../log/log.js';
3
+ export const LOG_CAP_BYTES = 64 * 1024;
4
+ const TRUNCATED = '\n[log truncated at 64 KB]\n';
5
+ const current = new AsyncLocalStorage();
6
+ export const withRunBuffer = (buffer, fn) => current.run(buffer, fn);
7
+ // Newlines kept; past the cap the rest is dropped and one marker line says so.
8
+ export const append = (buffer, line) => {
9
+ if (buffer.truncated)
10
+ return;
11
+ const next = `${buffer.text}${line}\n`;
12
+ if (Buffer.byteLength(next) <= LOG_CAP_BYTES - TRUNCATED.length) {
13
+ buffer.text = next;
14
+ return;
15
+ }
16
+ buffer.text = `${buffer.text}${TRUNCATED}`;
17
+ buffer.truncated = true;
18
+ };
19
+ const record = (level, message) => {
20
+ const buffer = current.getStore();
21
+ if (buffer)
22
+ append(buffer, `${level} ${message}`);
23
+ };
24
+ // ctx.log for a worker: the usual JSON line, and inside a run also the run's own log.
25
+ export const runLog = (base) => ({
26
+ info: (message, fields) => {
27
+ record('info', message);
28
+ base.info(message, fields);
29
+ },
30
+ warn: (message, fields) => {
31
+ record('warn', message);
32
+ base.warn(message, fields);
33
+ },
34
+ error: (error, fields) => {
35
+ record('error', messageOf(error));
36
+ base.error(error, fields);
37
+ },
38
+ });
@@ -0,0 +1,20 @@
1
+ import { type Store } from '../store/store.types.js';
2
+ export type RunStatus = 'ok' | 'failed';
3
+ export interface JobRun {
4
+ job: string;
5
+ startedAt: Date;
6
+ endedAt: Date;
7
+ status: RunStatus;
8
+ log: string;
9
+ timeoutMs?: number;
10
+ }
11
+ export interface RunStore {
12
+ save(run: JobRun): Promise<void>;
13
+ last(job: string): Promise<JobRun | undefined>;
14
+ }
15
+ export declare const KEEP_RUNS = 50;
16
+ export declare const RUNS_TABLE = "create table if not exists agentage_job_runs (\n id bigint generated always as identity primary key,\n job text not null,\n started_at timestamptz not null,\n ended_at timestamptz not null,\n status text not null,\n log text not null,\n timeout_ms integer\n)";
17
+ export declare const storeRuns: (store: Store) => RunStore;
18
+ export declare const memoryRuns: () => RunStore & {
19
+ all(job: string): JobRun[];
20
+ };
@@ -0,0 +1,59 @@
1
+ import {} from '../store/store.types.js';
2
+ export const KEEP_RUNS = 50;
3
+ export const RUNS_TABLE = `create table if not exists agentage_job_runs (
4
+ id bigint generated always as identity primary key,
5
+ job text not null,
6
+ started_at timestamptz not null,
7
+ ended_at timestamptz not null,
8
+ status text not null,
9
+ log text not null,
10
+ timeout_ms integer
11
+ )`;
12
+ const INSERT = `insert into agentage_job_runs (job, started_at, ended_at, status, log, timeout_ms)
13
+ values ($1, $2, $3, $4, $5, $6)`;
14
+ const PRUNE = `delete from agentage_job_runs where job = $1 and id not in
15
+ (select id from agentage_job_runs where job = $1 order by id desc limit ${KEEP_RUNS})`;
16
+ const fromRow = (row) => ({
17
+ job: row.job,
18
+ startedAt: new Date(row.started_at),
19
+ endedAt: new Date(row.ended_at),
20
+ status: row.status,
21
+ log: row.log,
22
+ ...(row.timeout_ms === null ? {} : { timeoutMs: row.timeout_ms }),
23
+ });
24
+ // Runs live in the extension's own store, in a table the kit owns; the last 50 per job are kept.
25
+ export const storeRuns = (store) => {
26
+ let table;
27
+ const ready = () => (table ??= store.query(RUNS_TABLE));
28
+ return {
29
+ save: async (run) => {
30
+ await ready();
31
+ const values = [
32
+ run.job,
33
+ run.startedAt,
34
+ run.endedAt,
35
+ run.status,
36
+ run.log,
37
+ run.timeoutMs ?? null,
38
+ ];
39
+ await store.query(INSERT, values);
40
+ await store.query(PRUNE, [run.job]);
41
+ },
42
+ last: async (job) => {
43
+ await ready();
44
+ const sql = 'select * from agentage_job_runs where job = $1 order by id desc limit 1';
45
+ const { rows } = await store.query(sql, [job]);
46
+ return rows[0] ? fromRow(rows[0]) : undefined;
47
+ },
48
+ };
49
+ };
50
+ // Without a store the runs live in memory, same rule.
51
+ export const memoryRuns = () => {
52
+ const runs = new Map();
53
+ const all = (job) => runs.get(job) ?? [];
54
+ return {
55
+ save: async (run) => void runs.set(run.job, [...all(run.job), run].slice(-KEEP_RUNS)),
56
+ last: async (job) => all(job).at(-1),
57
+ all,
58
+ };
59
+ };
@@ -0,0 +1,20 @@
1
+ import { type HealthCheck } from '../health/health.js';
2
+ import { type Log } from '../log/log.js';
3
+ import { type JobOptions } from './job-options.js';
4
+ import { type JobRun, type RunStore } from './runs.js';
5
+ export type Job = () => unknown;
6
+ export type Schedule = (name: string, cron: string, job: Job, options?: JobOptions) => void;
7
+ export interface SchedulerDeps {
8
+ runs: RunStore;
9
+ log: Log;
10
+ now?: () => Date;
11
+ }
12
+ export interface Scheduler {
13
+ schedule: Schedule;
14
+ start(): void;
15
+ stop(): void;
16
+ run(name: string): Promise<JobRun | undefined>;
17
+ checks(): Record<string, HealthCheck>;
18
+ }
19
+ export declare const LATE_MS = 60000;
20
+ export declare const createScheduler: (deps: SchedulerDeps) => Scheduler;