@agentage/extension 0.1.2 → 0.2.1
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 +6 -5
- package/dist/agcid/current.d.ts +7 -0
- package/dist/agcid/current.js +4 -0
- package/dist/agcid/scope.d.ts +0 -1
- package/dist/agcid/scope.js +2 -4
- package/dist/auth/route-access.d.ts +2 -0
- package/dist/auth/route-access.js +21 -0
- package/dist/auth/token-check.d.ts +10 -0
- package/dist/auth/token-check.js +18 -0
- package/dist/command/args.d.ts +14 -0
- package/dist/command/args.js +60 -0
- package/dist/command/check-cli.d.ts +1 -0
- package/dist/command/check-cli.js +19 -0
- package/dist/command/command.types.d.ts +14 -0
- package/dist/command/command.types.js +2 -0
- package/dist/command/out.d.ts +19 -0
- package/dist/command/out.js +32 -0
- package/dist/command/run-command.d.ts +16 -0
- package/dist/command/run-command.js +43 -0
- package/dist/context/context.d.ts +8 -14
- package/dist/context/context.js +20 -26
- package/dist/context/settings.d.ts +5 -0
- package/dist/context/settings.js +32 -0
- package/dist/context/test-context.d.ts +44 -0
- package/dist/context/test-context.js +75 -0
- package/dist/discovery/discovery.d.ts +12 -1
- package/dist/discovery/discovery.js +15 -6
- package/dist/discovery/discovery.schema.d.ts +7 -0
- package/dist/discovery/discovery.schema.js +4 -2
- package/dist/discovery/openapi.d.ts +3 -0
- package/dist/discovery/openapi.js +19 -0
- package/dist/health/health.schema.js +1 -1
- package/dist/index.d.ts +16 -3
- package/dist/index.js +16 -3
- package/dist/log/log.js +1 -1
- package/dist/manifest/entry.d.ts +5 -0
- package/dist/manifest/entry.js +29 -0
- package/dist/manifest/manifest.schema.d.ts +18 -0
- package/dist/manifest/manifest.schema.js +1 -0
- package/dist/manifest/parts.d.ts +1 -0
- package/dist/manifest/parts.js +1 -0
- package/dist/mcp/mcp-app.d.ts +8 -0
- package/dist/mcp/mcp-app.js +94 -0
- package/dist/mcp/server.d.ts +8 -0
- package/dist/mcp/server.js +38 -0
- package/dist/mcp/snapshot.d.ts +11 -0
- package/dist/mcp/snapshot.js +69 -0
- package/dist/mcp/tools.d.ts +26 -0
- package/dist/mcp/tools.js +21 -0
- package/dist/pages/pages-app.d.ts +8 -0
- package/dist/pages/pages-app.js +29 -0
- package/dist/pages/pages.d.ts +4 -0
- package/dist/pages/pages.js +16 -0
- package/dist/runtime/part-runners.d.ts +20 -0
- package/dist/runtime/part-runners.js +63 -0
- package/dist/runtime/run-part.d.ts +7 -3
- package/dist/runtime/run-part.js +67 -29
- package/dist/server/app.d.ts +20 -2
- package/dist/server/app.js +39 -11
- package/dist/store/migrate.d.ts +2 -0
- package/dist/store/migrate.js +22 -0
- package/dist/store/migrations.d.ts +12 -0
- package/dist/store/migrations.js +81 -0
- package/dist/store/store.d.ts +16 -0
- package/dist/store/store.js +55 -0
- package/dist/store/store.types.d.ts +7 -0
- package/dist/store/store.types.js +1 -0
- package/dist/store/test-store.d.ts +5 -0
- package/dist/store/test-store.js +39 -0
- package/dist/testing/test-server.d.ts +6 -0
- package/dist/testing/test-server.js +31 -9
- package/dist/tool/build.d.ts +4 -0
- package/dist/tool/build.js +21 -26
- package/dist/tool/check.d.ts +16 -0
- package/dist/tool/check.js +30 -0
- package/dist/tool/docker.js +3 -2
- package/dist/tool/exec.d.ts +5 -0
- package/dist/tool/exec.js +48 -9
- package/dist/tool/index.d.ts +8 -0
- package/dist/tool/index.js +8 -0
- package/dist/tool/kit-version.d.ts +1 -0
- package/dist/tool/kit-version.js +3 -0
- package/dist/tool/main.d.ts +6 -0
- package/dist/tool/main.js +60 -35
- package/dist/tool/record-extras.d.ts +4 -0
- package/dist/tool/record-extras.js +56 -0
- package/dist/tool/stage.d.ts +4 -0
- package/dist/tool/stage.js +39 -21
- package/dist/tool/timeouts.d.ts +10 -0
- package/dist/tool/timeouts.js +19 -0
- package/dist/tool/up/compose-run.d.ts +15 -0
- package/dist/tool/up/compose-run.js +25 -0
- package/dist/tool/up/compose.d.ts +19 -0
- package/dist/tool/up/compose.js +80 -0
- package/dist/tool/up/dev-issuer-app.d.ts +22 -0
- package/dist/tool/up/dev-issuer-app.js +80 -0
- package/dist/tool/up/dev-issuer.d.ts +2 -0
- package/dist/tool/up/dev-issuer.js +6 -0
- package/dist/tool/up/down.d.ts +13 -0
- package/dist/tool/up/down.js +47 -0
- package/dist/tool/up/gateway.d.ts +36 -0
- package/dist/tool/up/gateway.js +60 -0
- package/dist/tool/up/lines.d.ts +2 -0
- package/dist/tool/up/lines.js +3 -0
- package/dist/tool/up/local-record.d.ts +44 -0
- package/dist/tool/up/local-record.js +28 -0
- package/dist/tool/up/ports.d.ts +2 -0
- package/dist/tool/up/ports.js +22 -0
- package/dist/tool/up/project.d.ts +4 -0
- package/dist/tool/up/project.js +20 -0
- package/dist/tool/up/store-line.d.ts +11 -0
- package/dist/tool/up/store-line.js +36 -0
- package/dist/tool/up/token.d.ts +6 -0
- package/dist/tool/up/token.js +25 -0
- package/dist/tool/up/up.d.ts +16 -0
- package/dist/tool/up/up.js +84 -0
- package/dist/tool/up/wait.d.ts +8 -0
- package/dist/tool/up/wait.js +26 -0
- package/dist/ui/ui-app.d.ts +8 -0
- package/dist/ui/ui-app.js +39 -0
- package/dist/worker/cron.d.ts +11 -0
- package/dist/worker/cron.js +73 -0
- package/dist/worker/job-options.d.ts +9 -0
- package/dist/worker/job-options.js +17 -0
- package/dist/worker/run-log.d.ts +9 -0
- package/dist/worker/run-log.js +38 -0
- package/dist/worker/runs.d.ts +20 -0
- package/dist/worker/runs.js +59 -0
- package/dist/worker/scheduler.d.ts +20 -0
- package/dist/worker/scheduler.js +106 -0
- package/dist/worker/worker.types.d.ts +5 -0
- package/dist/worker/worker.types.js +2 -0
- package/package.json +11 -2
- package/schemas/extension-discovery.schema.json +5 -0
- package/schemas/extension-package.schema.json +3 -0
- package/schemas/extension-record.schema.json +5 -0
- package/schemas/health.schema.json +1 -1
package/README.md
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
# @agentage/extension
|
|
2
2
|
|
|
3
|
-
The agentage extension kit.
|
|
4
|
-
port, `/health`, the discovery document, the `agcid`
|
|
5
|
-
|
|
3
|
+
The agentage extension kit. Each part module (pages, api, mcp, worker, cli) exports only
|
|
4
|
+
`create(ctx)`; the kit owns the server, port, `/health`, the discovery document, the `agcid`
|
|
5
|
+
correlation id, the response envelope, the store pool, the scheduler and argument parsing.
|
|
6
|
+
`@agentage/extension/tool` is the tool behind `agentage extension build | up | down | token`.
|
|
6
7
|
|
|
7
8
|
```bash
|
|
8
9
|
npm install @agentage/extension express zod
|
|
9
10
|
```
|
|
10
11
|
|
|
11
|
-
See the [platform README](https://github.com/agentage/platform#readme) for
|
|
12
|
-
manifest, the
|
|
12
|
+
See the [platform README](https://github.com/agentage/platform#readme) for every part kind, the
|
|
13
|
+
manifest, the tool and how a part image runs.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
|
+
// Apart from the middleware, so code the cli loads (logs, the build tool) never imports express.
|
|
3
|
+
export const requestScope = new AsyncLocalStorage();
|
|
4
|
+
export const currentAgcid = () => requestScope.getStore()?.agcid;
|
package/dist/agcid/scope.d.ts
CHANGED
package/dist/agcid/scope.js
CHANGED
|
@@ -1,14 +1,12 @@
|
|
|
1
|
-
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
2
1
|
import {} from 'express';
|
|
3
2
|
import { AGCID_HEADER, agcidOf } from './agcid.js';
|
|
4
|
-
|
|
5
|
-
export const currentAgcid = () => scope.getStore()?.agcid;
|
|
3
|
+
import { requestScope } from './current.js';
|
|
6
4
|
// Keeps a valid incoming agcid, else mints one; echoed on every response and tags every log line.
|
|
7
5
|
export const agcidMiddleware = (req, res, next) => {
|
|
8
6
|
const agcid = agcidOf(req.get(AGCID_HEADER));
|
|
9
7
|
res.locals['agcid'] = agcid;
|
|
10
8
|
res.setHeader(AGCID_HEADER, agcid);
|
|
11
|
-
|
|
9
|
+
requestScope.run({ agcid }, next);
|
|
12
10
|
};
|
|
13
11
|
export const agcidOfResponse = (res) => {
|
|
14
12
|
const agcid = res.locals['agcid'];
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { accessOfSecurity } from './token-check.js';
|
|
2
|
+
const PARAM = /^\{.+\}$/;
|
|
3
|
+
const sameShape = (template, path) => {
|
|
4
|
+
const want = template.split('/');
|
|
5
|
+
const got = path.split('/');
|
|
6
|
+
return want.length === got.length && want.every((seg, i) => PARAM.test(seg) || seg === got[i]);
|
|
7
|
+
};
|
|
8
|
+
const params = (template) => template.split('/').filter((seg) => PARAM.test(seg)).length;
|
|
9
|
+
// As OpenAPI matches: a concrete path before a template, then the template with the fewest params.
|
|
10
|
+
const mostSpecific = (templates, path) => templates
|
|
11
|
+
.filter((template) => sameShape(template, path))
|
|
12
|
+
.sort((a, b) => params(a) - params(b))[0];
|
|
13
|
+
// The access the committed OpenAPI declares for a request: its operation's, else the document's.
|
|
14
|
+
export const routeAccess = (doc, method, path) => {
|
|
15
|
+
const paths = (doc?.['paths'] ?? {});
|
|
16
|
+
const template = mostSpecific(Object.keys(paths), path);
|
|
17
|
+
const operation = template ? paths[template]?.[method.toLowerCase()] : undefined;
|
|
18
|
+
if (!operation)
|
|
19
|
+
return 'read';
|
|
20
|
+
return accessOfSecurity(operation.security ?? doc?.['security']);
|
|
21
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export type Access = 'public' | 'read' | 'write' | 'admin';
|
|
2
|
+
export interface Caller {
|
|
3
|
+
sub: string;
|
|
4
|
+
scopes: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export type TokenCheck = (authorization: string | undefined, access: Access) => Promise<Caller | null>;
|
|
7
|
+
export declare const setCaller: (request: object, caller: Caller | null) => void;
|
|
8
|
+
export declare const callerOf: (request: object) => Caller | null;
|
|
9
|
+
export declare const strongest: (needed: readonly Access[]) => Access;
|
|
10
|
+
export declare const accessOfSecurity: (security: unknown) => Access;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Kit-private until ctx.auth.caller(request) exists (M3): part code never reads it from the request.
|
|
2
|
+
const callers = new WeakMap();
|
|
3
|
+
export const setCaller = (request, caller) => void callers.set(request, caller);
|
|
4
|
+
export const callerOf = (request) => callers.get(request) ?? null;
|
|
5
|
+
const RANK = { public: 0, read: 1, write: 2, admin: 3 };
|
|
6
|
+
export const strongest = (needed) => needed.reduce((top, access) => (RANK[access] > RANK[top] ? access : top), 'public');
|
|
7
|
+
const VERB = /:(read|write|admin)$/;
|
|
8
|
+
// OpenAPI security: [] is public, a scope names the verb; a route that declares nothing needs read.
|
|
9
|
+
export const accessOfSecurity = (security) => {
|
|
10
|
+
if (Array.isArray(security) && security.length === 0)
|
|
11
|
+
return 'public';
|
|
12
|
+
const scopes = (Array.isArray(security) ? security : [])
|
|
13
|
+
.flatMap((requirement) => Object.values(requirement ?? {}))
|
|
14
|
+
.flat()
|
|
15
|
+
.filter((scope) => typeof scope === 'string');
|
|
16
|
+
const verbs = scopes.map((scope) => VERB.exec(scope)?.[1] ?? 'read');
|
|
17
|
+
return verbs.length === 0 ? 'read' : strongest(verbs);
|
|
18
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type CommandSpec } from '../manifest/manifest.schema.js';
|
|
2
|
+
export type ArgValue = string | number | boolean | undefined;
|
|
3
|
+
export interface ParsedArgs {
|
|
4
|
+
args: Record<string, ArgValue>;
|
|
5
|
+
json: boolean;
|
|
6
|
+
help: boolean;
|
|
7
|
+
}
|
|
8
|
+
type ArgSpec = NonNullable<CommandSpec['args']>[number];
|
|
9
|
+
export declare class UsageError extends Error {
|
|
10
|
+
}
|
|
11
|
+
export declare const isOptional: (arg: ArgSpec) => boolean;
|
|
12
|
+
export declare const parseCommandArgs: (spec: CommandSpec, argv: string[]) => ParsedArgs;
|
|
13
|
+
export declare const usage: (spec: CommandSpec) => string;
|
|
14
|
+
export {};
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { parseArgs } from 'node:util';
|
|
2
|
+
import {} from '../manifest/manifest.schema.js';
|
|
3
|
+
export class UsageError extends Error {
|
|
4
|
+
}
|
|
5
|
+
export const isOptional = (arg) => arg.optional === true || arg.required === false;
|
|
6
|
+
const coerce = (arg, raw) => {
|
|
7
|
+
if (arg.type === 'boolean')
|
|
8
|
+
return raw === true || raw === 'true';
|
|
9
|
+
if (arg.type === 'string')
|
|
10
|
+
return String(raw);
|
|
11
|
+
const value = Number(raw);
|
|
12
|
+
const ok = arg.type === 'integer' ? Number.isInteger(value) : Number.isFinite(value);
|
|
13
|
+
if (raw === true || !ok)
|
|
14
|
+
throw new UsageError(`${arg.name} must be ${arg.type === 'integer' ? 'an integer' : 'a number'}`);
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
const config = (spec, argv) => {
|
|
18
|
+
const options = {
|
|
19
|
+
json: { type: 'boolean' },
|
|
20
|
+
help: { type: 'boolean', short: 'h' },
|
|
21
|
+
};
|
|
22
|
+
for (const arg of spec.args ?? [])
|
|
23
|
+
options[arg.name] = { type: arg.type === 'boolean' ? 'boolean' : 'string' };
|
|
24
|
+
return { args: argv, options, allowPositionals: true, strict: true };
|
|
25
|
+
};
|
|
26
|
+
const parse = (spec, argv) => {
|
|
27
|
+
try {
|
|
28
|
+
return parseArgs(config(spec, argv));
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
throw new UsageError(err.message);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
// Declared args come positional in order or as --name value; --json and --help belong to the kit.
|
|
35
|
+
export const parseCommandArgs = (spec, argv) => {
|
|
36
|
+
const { values, positionals } = parse(spec, argv);
|
|
37
|
+
const queue = [...positionals];
|
|
38
|
+
const args = {};
|
|
39
|
+
for (const arg of spec.args ?? []) {
|
|
40
|
+
// A boolean is a flag: absent reads false, so it is never a missing argument.
|
|
41
|
+
const raw = values[arg.name] ?? (arg.type === 'boolean' ? false : queue.shift());
|
|
42
|
+
if (raw === undefined && !isOptional(arg))
|
|
43
|
+
throw new UsageError(`missing argument: ${arg.name}`);
|
|
44
|
+
args[arg.name] = raw === undefined ? undefined : coerce(arg, raw);
|
|
45
|
+
}
|
|
46
|
+
if (queue.length > 0)
|
|
47
|
+
throw new UsageError(`too many arguments: ${queue.join(' ')}`);
|
|
48
|
+
return { args, json: values['json'] === true, help: values['help'] === true };
|
|
49
|
+
};
|
|
50
|
+
export const usage = (spec) => {
|
|
51
|
+
const names = (spec.args ?? []).map((a) => (isOptional(a) ? `[${a.name}]` : `<${a.name}>`));
|
|
52
|
+
const lines = [
|
|
53
|
+
`usage: agentage ${[spec.name, ...names].join(' ')} [--json]`,
|
|
54
|
+
'',
|
|
55
|
+
` ${spec.summary}`,
|
|
56
|
+
];
|
|
57
|
+
for (const arg of spec.args ?? [])
|
|
58
|
+
lines.push(` ${arg.name.padEnd(12)}${arg.type}${arg.description ? ` ${arg.description}` : ''}`);
|
|
59
|
+
return lines.join('\n');
|
|
60
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare const checkCli: (dir: string) => Promise<string | null>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { readManifest } from '../manifest/read-manifest.js';
|
|
2
|
+
import { createOut } from './out.js';
|
|
3
|
+
import { contextFor, loadHandlers } from './run-command.js';
|
|
4
|
+
const quiet = () => undefined;
|
|
5
|
+
// null when the cli entry exports create and its handlers are exactly the declared commands.
|
|
6
|
+
export const checkCli = async (dir) => {
|
|
7
|
+
try {
|
|
8
|
+
const pkg = await readManifest(dir);
|
|
9
|
+
const declared = (pkg.agentage.commands ?? []).map((command) => command.name).sort();
|
|
10
|
+
const out = createOut({ json: false, stdout: quiet, stderr: quiet });
|
|
11
|
+
const handlers = Object.keys(await loadHandlers(dir, pkg, contextFor(pkg, {}, out))).sort();
|
|
12
|
+
if (JSON.stringify(handlers) === JSON.stringify(declared))
|
|
13
|
+
return null;
|
|
14
|
+
return `cli handlers [${handlers.join(', ')}] differ from commands [${declared.join(', ')}]`;
|
|
15
|
+
}
|
|
16
|
+
catch (err) {
|
|
17
|
+
return err.message;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { type Out } from './out.js';
|
|
2
|
+
import { type ArgValue } from './args.js';
|
|
3
|
+
export interface CommandExtension {
|
|
4
|
+
id: string;
|
|
5
|
+
version: string;
|
|
6
|
+
part: 'cli';
|
|
7
|
+
}
|
|
8
|
+
export interface CliContext {
|
|
9
|
+
readonly extension: Readonly<CommandExtension>;
|
|
10
|
+
readonly args: Readonly<Record<string, ArgValue>>;
|
|
11
|
+
readonly out: Out;
|
|
12
|
+
}
|
|
13
|
+
export type CommandHandler = () => unknown;
|
|
14
|
+
export type CliCreate = (ctx: CliContext) => Record<string, CommandHandler>;
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export interface Out {
|
|
2
|
+
log(...data: unknown[]): void;
|
|
3
|
+
warn(...data: unknown[]): void;
|
|
4
|
+
error(...data: unknown[]): void;
|
|
5
|
+
table(rows: readonly Record<string, unknown>[]): void;
|
|
6
|
+
json(value: unknown): void;
|
|
7
|
+
fail(code: number, message: string): void;
|
|
8
|
+
}
|
|
9
|
+
export type Write = (text: string) => void;
|
|
10
|
+
export interface OutOptions {
|
|
11
|
+
json: boolean;
|
|
12
|
+
stdout: Write;
|
|
13
|
+
stderr: Write;
|
|
14
|
+
}
|
|
15
|
+
export interface RecordedOut extends Out {
|
|
16
|
+
readonly exitCode: number | undefined;
|
|
17
|
+
}
|
|
18
|
+
export declare const tableLines: (rows: readonly Record<string, unknown>[]) => string[];
|
|
19
|
+
export declare const createOut: (options: OutOptions) => RecordedOut;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { format } from 'node:util';
|
|
2
|
+
const cell = (value) => (typeof value === 'string' ? value : format('%o', value));
|
|
3
|
+
// Columns padded to the widest cell, a header row from the keys of every row.
|
|
4
|
+
export const tableLines = (rows) => {
|
|
5
|
+
const keys = [...new Set(rows.flatMap((row) => Object.keys(row)))];
|
|
6
|
+
const body = rows.map((row) => keys.map((key) => (key in row ? cell(row[key]) : '')));
|
|
7
|
+
const widths = keys.map((key, i) => Math.max(key.length, ...body.map((cells) => cells[i]?.length ?? 0)));
|
|
8
|
+
const line = (cells) => cells
|
|
9
|
+
.map((c, i) => c.padEnd(widths[i] ?? 0))
|
|
10
|
+
.join(' ')
|
|
11
|
+
.trimEnd();
|
|
12
|
+
return [line(keys), ...body.map(line)];
|
|
13
|
+
};
|
|
14
|
+
// Reads like the console; under --json stdout carries out.json only, the rest goes to stderr.
|
|
15
|
+
export const createOut = (options) => {
|
|
16
|
+
let exitCode;
|
|
17
|
+
const text = options.json ? options.stderr : options.stdout;
|
|
18
|
+
return {
|
|
19
|
+
log: (...data) => text(`${format(...data)}\n`),
|
|
20
|
+
warn: (...data) => options.stderr(`${format(...data)}\n`),
|
|
21
|
+
error: (...data) => options.stderr(`${format(...data)}\n`),
|
|
22
|
+
table: (rows) => text(`${tableLines(rows).join('\n')}\n`),
|
|
23
|
+
json: (value) => options.stdout(`${JSON.stringify(value, null, 2)}\n`),
|
|
24
|
+
fail: (code, message) => {
|
|
25
|
+
exitCode = code;
|
|
26
|
+
options.stderr(`${message}\n`);
|
|
27
|
+
},
|
|
28
|
+
get exitCode() {
|
|
29
|
+
return exitCode;
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
2
|
+
import { type CliContext, type CommandHandler } from './command.types.js';
|
|
3
|
+
import { type Write } from './out.js';
|
|
4
|
+
export interface RunCommandOptions {
|
|
5
|
+
dir: string;
|
|
6
|
+
command: string;
|
|
7
|
+
argv: string[];
|
|
8
|
+
stdout?: Write;
|
|
9
|
+
stderr?: Write;
|
|
10
|
+
}
|
|
11
|
+
export declare const EXIT_OK = 0;
|
|
12
|
+
export declare const EXIT_FAILED = 1;
|
|
13
|
+
export declare const EXIT_USAGE = 2;
|
|
14
|
+
export declare const loadHandlers: (dir: string, pkg: ExtensionPackage, ctx: CliContext) => Promise<Record<string, CommandHandler>>;
|
|
15
|
+
export declare const contextFor: (pkg: ExtensionPackage, args: CliContext["args"], out: CliContext["out"]) => CliContext;
|
|
16
|
+
export declare const runCommand: (o: RunCommandOptions) => Promise<number>;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { loadCreate } from '../manifest/entry.js';
|
|
2
|
+
import {} from '../manifest/manifest.schema.js';
|
|
3
|
+
import { readManifest } from '../manifest/read-manifest.js';
|
|
4
|
+
import { parseCommandArgs, usage, UsageError } from './args.js';
|
|
5
|
+
import {} from './command.types.js';
|
|
6
|
+
import { createOut } from './out.js';
|
|
7
|
+
export const EXIT_OK = 0;
|
|
8
|
+
export const EXIT_FAILED = 1;
|
|
9
|
+
export const EXIT_USAGE = 2;
|
|
10
|
+
const writeOut = (text) => void process.stdout.write(text);
|
|
11
|
+
const writeErr = (text) => void process.stderr.write(text);
|
|
12
|
+
export const loadHandlers = async (dir, pkg, ctx) => (await loadCreate(dir, pkg, 'cli'))(ctx);
|
|
13
|
+
export const contextFor = (pkg, args, out) => ({
|
|
14
|
+
extension: Object.freeze({ id: pkg.agentage.id, version: pkg.version, part: 'cli' }),
|
|
15
|
+
args: Object.freeze(args),
|
|
16
|
+
out,
|
|
17
|
+
});
|
|
18
|
+
const execute = async (o, pkg, stdout, stderr) => {
|
|
19
|
+
const spec = pkg.agentage.commands?.find((command) => command.name === o.command);
|
|
20
|
+
if (!spec)
|
|
21
|
+
throw new UsageError(`${pkg.agentage.id} has no command ${o.command}`);
|
|
22
|
+
const parsed = parseCommandArgs(spec, o.argv);
|
|
23
|
+
if (parsed.help)
|
|
24
|
+
return (stdout(`${usage(spec)}\n`), EXIT_OK);
|
|
25
|
+
const out = createOut({ json: parsed.json, stdout, stderr });
|
|
26
|
+
const handler = (await loadHandlers(o.dir, pkg, contextFor(pkg, parsed.args, out)))[o.command];
|
|
27
|
+
if (typeof handler !== 'function')
|
|
28
|
+
throw new Error(`the cli entry has no handler for ${o.command}`);
|
|
29
|
+
const result = await handler();
|
|
30
|
+
return out.exitCode ?? (typeof result === 'number' ? result : EXIT_OK);
|
|
31
|
+
};
|
|
32
|
+
// Exit codes: 0 ok, 2 usage, 1 failure (a throw, a missing handler, an invalid manifest).
|
|
33
|
+
export const runCommand = async (o) => {
|
|
34
|
+
const stdout = o.stdout ?? writeOut;
|
|
35
|
+
const stderr = o.stderr ?? writeErr;
|
|
36
|
+
try {
|
|
37
|
+
return await execute(o, await readManifest(o.dir), stdout, stderr);
|
|
38
|
+
}
|
|
39
|
+
catch (err) {
|
|
40
|
+
stderr(`${err.message}\n`);
|
|
41
|
+
return err instanceof UsageError ? EXIT_USAGE : EXIT_FAILED;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
2
|
-
import { type Log
|
|
2
|
+
import { type Log } from '../log/log.js';
|
|
3
|
+
import { type Store } from '../store/store.types.js';
|
|
3
4
|
export type ServerPartKind = 'pages' | 'api' | 'mcp' | 'worker';
|
|
4
5
|
export type SettingValue = string | number | boolean;
|
|
5
6
|
export interface ExtensionInfo {
|
|
@@ -7,30 +8,23 @@ export interface ExtensionInfo {
|
|
|
7
8
|
version: string;
|
|
8
9
|
part: ServerPartKind;
|
|
9
10
|
origin: string;
|
|
11
|
+
commit: string;
|
|
10
12
|
}
|
|
11
13
|
export interface PartContext {
|
|
12
14
|
readonly extension: Readonly<ExtensionInfo>;
|
|
13
15
|
readonly log: Log;
|
|
14
16
|
readonly settings: Readonly<Record<string, SettingValue>>;
|
|
17
|
+
readonly store: Store;
|
|
15
18
|
}
|
|
19
|
+
export declare const NO_STORE: Store;
|
|
16
20
|
export declare const settingDefaults: (pkg: ExtensionPackage) => Record<string, SettingValue>;
|
|
17
21
|
export interface ContextInput {
|
|
18
22
|
pkg: ExtensionPackage;
|
|
19
23
|
part: ServerPartKind;
|
|
20
24
|
origin: string;
|
|
25
|
+
commit: string;
|
|
21
26
|
log?: Log;
|
|
22
|
-
}
|
|
23
|
-
export declare const createContext: ({ pkg, part, origin, log }: ContextInput) => PartContext;
|
|
24
|
-
export interface LogEntry {
|
|
25
|
-
level: LogLevel;
|
|
26
|
-
message: string;
|
|
27
|
-
fields: LogFields;
|
|
28
|
-
}
|
|
29
|
-
export interface TestContext extends PartContext {
|
|
30
|
-
readonly logs: LogEntry[];
|
|
31
|
-
}
|
|
32
|
-
export interface TestContextOptions {
|
|
33
|
-
extension?: Partial<ExtensionInfo>;
|
|
34
27
|
settings?: Record<string, SettingValue>;
|
|
28
|
+
store?: Store;
|
|
35
29
|
}
|
|
36
|
-
export declare const
|
|
30
|
+
export declare const createContext: (input: ContextInput) => PartContext;
|
package/dist/context/context.js
CHANGED
|
@@ -1,29 +1,23 @@
|
|
|
1
1
|
import {} from '../manifest/manifest.schema.js';
|
|
2
|
-
import { createLog
|
|
2
|
+
import { createLog } from '../log/log.js';
|
|
3
|
+
import {} from '../store/store.types.js';
|
|
4
|
+
// ctx.store is always there; without a store part in the manifest it says so on first use.
|
|
5
|
+
export const NO_STORE = {
|
|
6
|
+
query: async () => {
|
|
7
|
+
throw new Error('this extension declares no store: add parts.store to the manifest');
|
|
8
|
+
},
|
|
9
|
+
};
|
|
3
10
|
export const settingDefaults = (pkg) => Object.fromEntries(Object.entries(pkg.agentage.settings ?? {}).map(([key, setting]) => [key, setting.default]));
|
|
4
|
-
export const createContext = (
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
}),
|
|
17
|
-
});
|
|
18
|
-
// A ctx for unit tests: fixed identity, settings as given, a log that records what it would print.
|
|
19
|
-
export const createTestContext = (options = {}) => {
|
|
20
|
-
const logs = [];
|
|
21
|
-
const extension = {
|
|
22
|
-
id: 'test',
|
|
23
|
-
version: '0.0.0',
|
|
24
|
-
part: 'api',
|
|
25
|
-
origin: 'http://127.0.0.1',
|
|
26
|
-
...options.extension,
|
|
27
|
-
};
|
|
28
|
-
return { extension, log: recordingLog(logs), settings: { ...options.settings }, logs };
|
|
11
|
+
export const createContext = (input) => {
|
|
12
|
+
const { pkg, part, origin, commit, store } = input;
|
|
13
|
+
const extension = Object.freeze({
|
|
14
|
+
id: pkg.agentage.id,
|
|
15
|
+
version: pkg.version,
|
|
16
|
+
part,
|
|
17
|
+
origin,
|
|
18
|
+
commit,
|
|
19
|
+
});
|
|
20
|
+
const log = input.log ?? createLog({ extension: pkg.agentage.id, part });
|
|
21
|
+
const settings = Object.freeze({ ...settingDefaults(pkg), ...input.settings });
|
|
22
|
+
return Object.freeze({ extension, log, settings, store: store ?? NO_STORE });
|
|
29
23
|
};
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { type Log } from '../log/log.js';
|
|
2
|
+
import { type ExtensionPackage } from '../manifest/manifest.schema.js';
|
|
3
|
+
import { type Env } from '../runtime/build-info.js';
|
|
4
|
+
import { type SettingValue } from './context.js';
|
|
5
|
+
export declare const settingsFromEnv: (pkg: ExtensionPackage, env: Env, log: Log) => Record<string, SettingValue>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import {} from '../log/log.js';
|
|
2
|
+
import {} from '../manifest/manifest.schema.js';
|
|
3
|
+
import {} from '../runtime/build-info.js';
|
|
4
|
+
import { settingDefaults } from './context.js';
|
|
5
|
+
const fits = (setting, value) => {
|
|
6
|
+
if (setting.type === 'integer' ? !Number.isInteger(value) : typeof value !== setting.type)
|
|
7
|
+
return false;
|
|
8
|
+
return !setting.enum || setting.enum.includes(String(value));
|
|
9
|
+
};
|
|
10
|
+
const parseObject = (text) => {
|
|
11
|
+
const value = JSON.parse(text);
|
|
12
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
13
|
+
throw new Error('AGENTAGE_SETTINGS is not a JSON object');
|
|
14
|
+
}
|
|
15
|
+
return value;
|
|
16
|
+
};
|
|
17
|
+
// The host's values (AGENTAGE_SETTINGS, JSON) over the defaults; a wrong type stops the start.
|
|
18
|
+
export const settingsFromEnv = (pkg, env, log) => {
|
|
19
|
+
const declared = pkg.agentage.settings ?? {};
|
|
20
|
+
const given = env['AGENTAGE_SETTINGS'] ? parseObject(env['AGENTAGE_SETTINGS']) : {};
|
|
21
|
+
const values = settingDefaults(pkg);
|
|
22
|
+
for (const [key, value] of Object.entries(given)) {
|
|
23
|
+
const setting = declared[key];
|
|
24
|
+
if (!setting)
|
|
25
|
+
log.warn('setting not declared in the manifest, ignored', { setting: key });
|
|
26
|
+
else if (fits(setting, value))
|
|
27
|
+
values[key] = value;
|
|
28
|
+
else
|
|
29
|
+
throw new Error(`setting ${key}: ${JSON.stringify(value)} does not fit its declaration`);
|
|
30
|
+
}
|
|
31
|
+
return values;
|
|
32
|
+
};
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { type CliContext } from '../command/command.types.js';
|
|
2
|
+
import { type LogFields, type LogLevel } from '../log/log.js';
|
|
3
|
+
import { type ToolEntry, type ToolRegistrar } from '../mcp/tools.js';
|
|
4
|
+
import { type JobRun } from '../worker/runs.js';
|
|
5
|
+
import { type Schedule } from '../worker/scheduler.js';
|
|
6
|
+
import { type ExtensionInfo, type PartContext, type SettingValue } from './context.js';
|
|
7
|
+
import { type ArgValue } from '../command/args.js';
|
|
8
|
+
export interface LogEntry {
|
|
9
|
+
level: LogLevel;
|
|
10
|
+
message: string;
|
|
11
|
+
fields: LogFields;
|
|
12
|
+
}
|
|
13
|
+
export interface TestContext extends PartContext {
|
|
14
|
+
readonly logs: LogEntry[];
|
|
15
|
+
close(): Promise<void>;
|
|
16
|
+
readonly schedule: Schedule;
|
|
17
|
+
readonly tool: ToolRegistrar;
|
|
18
|
+
readonly tools: ToolEntry[];
|
|
19
|
+
runJob(name: string): Promise<JobRun | undefined>;
|
|
20
|
+
}
|
|
21
|
+
export interface TestCommandContext extends CliContext {
|
|
22
|
+
readonly output: {
|
|
23
|
+
stdout: string[];
|
|
24
|
+
stderr: string[];
|
|
25
|
+
};
|
|
26
|
+
readonly exitCode: number | undefined;
|
|
27
|
+
}
|
|
28
|
+
export interface TestContextOptions {
|
|
29
|
+
extension?: Partial<ExtensionInfo>;
|
|
30
|
+
settings?: Record<string, SettingValue>;
|
|
31
|
+
store?: boolean | {
|
|
32
|
+
dir: string;
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export interface TestCommandOptions {
|
|
36
|
+
args: Record<string, ArgValue>;
|
|
37
|
+
extension?: {
|
|
38
|
+
id?: string;
|
|
39
|
+
version?: string;
|
|
40
|
+
};
|
|
41
|
+
json?: boolean;
|
|
42
|
+
}
|
|
43
|
+
export declare function createTestContext(options: TestCommandOptions): TestCommandContext;
|
|
44
|
+
export declare function createTestContext(options?: TestContextOptions): TestContext;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {} from '../command/command.types.js';
|
|
2
|
+
import { createOut } from '../command/out.js';
|
|
3
|
+
import { messageOf } from '../log/log.js';
|
|
4
|
+
import { createToolRegistry } from '../mcp/tools.js';
|
|
5
|
+
import { migrate } from '../store/migrations.js';
|
|
6
|
+
import { createTestStore } from '../store/test-store.js';
|
|
7
|
+
import { runLog } from '../worker/run-log.js';
|
|
8
|
+
import { memoryRuns } from '../worker/runs.js';
|
|
9
|
+
import { createScheduler } from '../worker/scheduler.js';
|
|
10
|
+
import { NO_STORE } from './context.js';
|
|
11
|
+
import {} from '../command/args.js';
|
|
12
|
+
const recordingLog = (logs) => ({
|
|
13
|
+
info: (message, fields = {}) => void logs.push({ level: 'info', message, fields }),
|
|
14
|
+
warn: (message, fields = {}) => void logs.push({ level: 'warn', message, fields }),
|
|
15
|
+
error: (error, fields = {}) => void logs.push({
|
|
16
|
+
level: 'error',
|
|
17
|
+
message: messageOf(error),
|
|
18
|
+
fields: { ...fields, err: error },
|
|
19
|
+
}),
|
|
20
|
+
});
|
|
21
|
+
const testStore = (option, log) => {
|
|
22
|
+
if (!option)
|
|
23
|
+
return undefined;
|
|
24
|
+
const dir = option === true ? process.cwd() : option.dir;
|
|
25
|
+
return createTestStore((store) => migrate(store, dir, log));
|
|
26
|
+
};
|
|
27
|
+
const TEST_EXTENSION = {
|
|
28
|
+
id: 'test',
|
|
29
|
+
version: '0.0.0',
|
|
30
|
+
part: 'api',
|
|
31
|
+
origin: 'http://127.0.0.1',
|
|
32
|
+
commit: '0'.repeat(40),
|
|
33
|
+
};
|
|
34
|
+
// Jobs run by hand with runJob, tools recorded as the mcp part registers them.
|
|
35
|
+
const recorders = (log) => {
|
|
36
|
+
const scheduler = createScheduler({ runs: memoryRuns(), log });
|
|
37
|
+
return { ...createToolRegistry(), schedule: scheduler.schedule, runJob: scheduler.run };
|
|
38
|
+
};
|
|
39
|
+
const serverContext = (options) => {
|
|
40
|
+
const logs = [];
|
|
41
|
+
const log = runLog(recordingLog(logs));
|
|
42
|
+
const store = testStore(options.store, recordingLog([]));
|
|
43
|
+
return {
|
|
44
|
+
extension: { ...TEST_EXTENSION, ...options.extension },
|
|
45
|
+
log,
|
|
46
|
+
logs,
|
|
47
|
+
settings: { ...options.settings },
|
|
48
|
+
store: store ?? NO_STORE,
|
|
49
|
+
close: async () => store?.close(),
|
|
50
|
+
...recorders(log),
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
// One recorded line per line written, the trailing newline dropped.
|
|
54
|
+
const lines = (into) => (text) => void into.push(...text.replace(/\n$/, '').split('\n'));
|
|
55
|
+
const commandContext = (options) => {
|
|
56
|
+
const output = { stdout: [], stderr: [] };
|
|
57
|
+
const out = createOut({
|
|
58
|
+
json: options.json ?? false,
|
|
59
|
+
stdout: lines(output.stdout),
|
|
60
|
+
stderr: lines(output.stderr),
|
|
61
|
+
});
|
|
62
|
+
const extension = { id: 'test', version: '0.0.0', ...options.extension, part: 'cli' };
|
|
63
|
+
return {
|
|
64
|
+
extension,
|
|
65
|
+
args: { ...options.args },
|
|
66
|
+
out,
|
|
67
|
+
output,
|
|
68
|
+
get exitCode() {
|
|
69
|
+
return out.exitCode;
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
export function createTestContext(options = {}) {
|
|
74
|
+
return 'args' in options ? commandContext(options) : serverContext(options);
|
|
75
|
+
}
|
|
@@ -7,7 +7,17 @@ export interface BuildInfo {
|
|
|
7
7
|
}
|
|
8
8
|
export type PartImages = Partial<Record<ImagePartKind, string>>;
|
|
9
9
|
export declare const DEFAULT_RECORD_FILE = "/run/agentage/agentage-extension.json";
|
|
10
|
-
export
|
|
10
|
+
export interface RecordExtras {
|
|
11
|
+
openapi?: string;
|
|
12
|
+
tools?: string[];
|
|
13
|
+
widgets?: {
|
|
14
|
+
name: string;
|
|
15
|
+
element: string;
|
|
16
|
+
module: string;
|
|
17
|
+
}[];
|
|
18
|
+
migrations?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare const recordOf: (pkg: ExtensionPackage, build: BuildInfo, images: PartImages, extras?: RecordExtras) => ExtensionRecord;
|
|
11
21
|
export declare const validateRecord: (value: unknown) => ExtensionRecord;
|
|
12
22
|
export declare const loadRecord: (file: string) => Promise<ExtensionRecord | null>;
|
|
13
23
|
export declare const discoveryDocument: (record: ExtensionRecord, host: string) => {
|
|
@@ -29,6 +39,7 @@ export declare const discoveryDocument: (record: ExtensionRecord, host: string)
|
|
|
29
39
|
} | undefined;
|
|
30
40
|
store?: {
|
|
31
41
|
kind: "postgres" | "git" | "signoz";
|
|
42
|
+
migrations?: number | undefined;
|
|
32
43
|
} | undefined;
|
|
33
44
|
};
|
|
34
45
|
openapi?: string | undefined;
|