@poietic-tech/cli 0.0.0-stage → 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/dist/errors.js ADDED
@@ -0,0 +1,139 @@
1
+ import { sanitize } from "./text.js";
2
+ import { PoieticAuthError } from "@poietic-tech/auth";
3
+ import { CredentialFileError, LoginDenied, LoginExpired, UnsupportedPlatform, UntrustedServer, } from "@poietic-tech/auth/node";
4
+ /** The exit-code table, verbatim. */
5
+ export const EXIT_ROWS = [
6
+ { exit: 0, meaning: "success", codes: [] },
7
+ { exit: 1, meaning: "unexpected failure", codes: ["internal", "module_failed"] },
8
+ { exit: 2, meaning: "usage error", codes: ["bad_usage", "bad_request"] },
9
+ { exit: 3, meaning: "not logged in, or the login was refused", codes: ["not_logged_in"] },
10
+ { exit: 4, meaning: "not found", codes: ["no_such_agent", "not_found"] },
11
+ {
12
+ exit: 5,
13
+ meaning: "not allowed",
14
+ codes: ["agent_not_allowed", "not_allowed", "not_a_builder", "cannot_delegate", "quota_exceeded", "too_many_tokens"],
15
+ },
16
+ { exit: 6, meaning: "a step-up is needed and `--no-wait` was given", codes: ["reauth_required"] },
17
+ { exit: 7, meaning: "the login or step-up was denied or expired", codes: ["login_denied", "login_expired"] },
18
+ {
19
+ exit: 8,
20
+ meaning: "network, server or local environment failure",
21
+ codes: ["network", "server_error", "untrusted_server", "credential_file", "unsupported_platform"],
22
+ },
23
+ { exit: 130, meaning: "interrupted by Ctrl-C", codes: ["interrupted"] },
24
+ ];
25
+ export const EXIT = Object.assign(Object.create(null), Object.fromEntries(EXIT_ROWS.flatMap((r) => r.codes.map((c) => [c, r.exit]))));
26
+ /** Exit code for a code the table does not name: by HTTP status. */
27
+ function exitByStatus(status) {
28
+ if (status === undefined)
29
+ return 1;
30
+ if (status === 400 || status === 409 || status === 422)
31
+ return 2;
32
+ if (status === 401)
33
+ return 3;
34
+ if (status === 403)
35
+ return 5;
36
+ if (status === 404)
37
+ return 4;
38
+ if (status === 0 || status === 429 || status >= 500)
39
+ return 8;
40
+ return 1;
41
+ }
42
+ export function exitCodeOf(code, status) {
43
+ return Object.hasOwn(EXIT, code) ? EXIT[code] : exitByStatus(status);
44
+ }
45
+ export class CliError extends Error {
46
+ code;
47
+ exitCode;
48
+ status;
49
+ constructor(code, message, opts = {}) {
50
+ super(message);
51
+ this.name = "CliError";
52
+ this.code = code;
53
+ this.exitCode = exitCodeOf(code, opts.status);
54
+ if (opts.status !== undefined)
55
+ this.status = opts.status;
56
+ }
57
+ }
58
+ const NETWORK_CODES = /^(ECONNREFUSED|ENOTFOUND|ECONNRESET|ETIMEDOUT)$/;
59
+ /** The code a status stands for when the server named none. */
60
+ export function codeByStatus(status) {
61
+ if (status === 401)
62
+ return "not_logged_in";
63
+ if (status === 403)
64
+ return "not_allowed";
65
+ if (status === 404)
66
+ return "not_found";
67
+ if (status === 400 || status === 409 || status === 422)
68
+ return "bad_request";
69
+ if (status === 0 || status === 429 || status >= 500)
70
+ return "server_error";
71
+ return "internal";
72
+ }
73
+ export function toCliError(error, signal) {
74
+ if (error instanceof CliError)
75
+ return error;
76
+ if (signal.aborted)
77
+ return new CliError("interrupted", "Interrupted.");
78
+ if (error instanceof LoginDenied)
79
+ return new CliError("login_denied", error.message, { status: error.status });
80
+ if (error instanceof LoginExpired)
81
+ return new CliError("login_expired", error.message, { status: error.status });
82
+ if (error instanceof UntrustedServer)
83
+ return new CliError("untrusted_server", error.message);
84
+ if (error instanceof CredentialFileError)
85
+ return new CliError("credential_file", error.message);
86
+ if (error instanceof UnsupportedPlatform)
87
+ return new CliError("unsupported_platform", error.message);
88
+ if (error instanceof PoieticAuthError) {
89
+ return new CliError(error.code ?? codeByStatus(error.status), error.message, { status: error.status });
90
+ }
91
+ if (error instanceof TypeError) {
92
+ const cause = error.cause;
93
+ if (/fetch failed/.test(error.message) || (typeof cause?.code === "string" && NETWORK_CODES.test(cause.code))) {
94
+ return new CliError("network", "Cannot reach the auth server.");
95
+ }
96
+ }
97
+ return new CliError("internal", error instanceof Error ? error.message : "Unexpected failure.");
98
+ }
99
+ /**
100
+ * A login credential, an agent token or a JWT, starting where a word starts: "compat_bot" is a
101
+ * name, not a token. "_" may precede one, so "ci_pat_…" is still caught, and so may a percent or
102
+ * backslash escape, so "code%3Dpcl_…" and a literal "\npat_…" are too.
103
+ */
104
+ const SECRET = /(?:(?<![A-Za-z0-9])|(?<=%[0-9A-Fa-f]{2})|(?<=\\[nrt])|(?<=\\u[0-9A-Fa-f]{4}))(?:pcl_[A-Za-z0-9_-]+|pat_[A-Za-z0-9_-]+|eyJ[\w-]*\.[\w-]+\.[\w-]*)/g;
105
+ export function redact(text) {
106
+ return text.replace(SECRET, "[redacted]");
107
+ }
108
+ export function renderError(e, machine, color) {
109
+ const message = redact(e.message);
110
+ if (machine) {
111
+ return JSON.stringify({ error: redact(e.code), message, ...(e.status !== undefined ? { status: e.status } : {}) }) + "\n";
112
+ }
113
+ return `${color ? "\x1b[31merror:\x1b[0m" : "error:"} ${sanitize(message)}\n`;
114
+ }
115
+ const API_CODE = /^[a-z][a-z0-9_]{0,63}$/;
116
+ const API_MESSAGES = Object.assign(Object.create(null), {
117
+ name_taken: "An agent with that name already exists.",
118
+ quota_exceeded: "You have reached your agent quota.",
119
+ too_many_tokens: "This agent already has the most live tokens it may have.",
120
+ not_allowed: "Your account may not manage agents.",
121
+ not_a_builder: "The agent you act as is not a builder.",
122
+ });
123
+ /** The error for a failed auth-server answer. The body is read for its code only, never quoted. */
124
+ export async function apiError(res, what) {
125
+ let code;
126
+ try {
127
+ const body = await res.json();
128
+ const c = body !== null && typeof body === "object" ? body.error : undefined;
129
+ // A secret-shaped "code" is no code: it would be printed as the error's name.
130
+ if (typeof c === "string" && API_CODE.test(c) && redact(c) === c)
131
+ code = c;
132
+ }
133
+ catch {
134
+ // not JSON: the status decides
135
+ }
136
+ const named = code ?? codeByStatus(res.status);
137
+ const message = API_MESSAGES[named] ?? `${what} failed: ${res.status} (${named})`;
138
+ return new CliError(named, message, { status: res.status });
139
+ }
@@ -0,0 +1,11 @@
1
+ /** What `onInterrupt` needs of `process`, so a test can stand in for it. */
2
+ export interface SignalSource {
3
+ on(event: "SIGINT", listener: () => void): unknown;
4
+ }
5
+ /**
6
+ * Ctrl-C. The first press aborts the run's signal, which every request, wait and sleep the host
7
+ * makes honours, so the command ends itself with exit 130 (and a login can still revoke the
8
+ * session it was collecting). A module's own work may not listen, so a second press exits 130 at
9
+ * once. Listening at all turns off Node's default exit, which is why the second press is needed.
10
+ */
11
+ export declare function onInterrupt(proc: SignalSource, controller: AbortController, exit: (code: number) => void): void;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Ctrl-C. The first press aborts the run's signal, which every request, wait and sleep the host
3
+ * makes honours, so the command ends itself with exit 130 (and a login can still revoke the
4
+ * session it was collecting). A module's own work may not listen, so a second press exits 130 at
5
+ * once. Listening at all turns off Node's default exit, which is why the second press is needed.
6
+ */
7
+ export function onInterrupt(proc, controller, exit) {
8
+ proc.on("SIGINT", () => {
9
+ if (controller.signal.aborted)
10
+ exit(130);
11
+ else
12
+ controller.abort();
13
+ });
14
+ }
package/dist/io.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ /** Everything the command touches outside its own process, injected so tests can fake it. */
2
+ export interface CliIO {
3
+ stdout(t: string): void;
4
+ stderr(t: string): void;
5
+ env: Readonly<Record<string, string | undefined>>;
6
+ cwd: string;
7
+ stdoutIsTTY: boolean;
8
+ stderrIsTTY: boolean;
9
+ now(): number;
10
+ sleep(ms: number, signal?: AbortSignal): Promise<void>;
11
+ openBrowser(url: string): void;
12
+ signal: AbortSignal;
13
+ hostname: string;
14
+ platform: string;
15
+ hostDir: string;
16
+ execPath: string;
17
+ }
18
+ export declare function processIO(signal: AbortSignal): CliIO;
package/dist/io.js ADDED
@@ -0,0 +1,48 @@
1
+ /// <reference types="node" />
2
+ import { spawn } from "node:child_process";
3
+ import { hostname } from "node:os";
4
+ import { dirname } from "node:path";
5
+ import { fileURLToPath } from "node:url";
6
+ export function processIO(signal) {
7
+ return {
8
+ stdout: (t) => void process.stdout.write(t),
9
+ stderr: (t) => void process.stderr.write(t),
10
+ env: process.env,
11
+ cwd: process.cwd(),
12
+ stdoutIsTTY: process.stdout.isTTY === true,
13
+ stderrIsTTY: process.stderr.isTTY === true,
14
+ now: () => Date.now() / 1000,
15
+ sleep: (ms, s) => new Promise((resolve, reject) => {
16
+ if (s?.aborted)
17
+ return reject(s.reason);
18
+ const onAbort = () => {
19
+ clearTimeout(timer);
20
+ reject(s.reason);
21
+ };
22
+ const timer = setTimeout(() => {
23
+ s?.removeEventListener("abort", onAbort);
24
+ resolve();
25
+ }, ms);
26
+ s?.addEventListener("abort", onAbort, { once: true });
27
+ }),
28
+ openBrowser: (url) => {
29
+ try {
30
+ const child = spawn(process.platform === "darwin" ? "open" : "xdg-open", [url], {
31
+ detached: true,
32
+ stdio: "ignore",
33
+ });
34
+ child.on("error", () => { });
35
+ child.unref();
36
+ }
37
+ catch {
38
+ // No browser to open: the link is printed anyway.
39
+ }
40
+ },
41
+ signal,
42
+ hostname: hostname(),
43
+ platform: process.platform,
44
+ // src/io.ts and dist/io.js both sit one level below the package directory.
45
+ hostDir: dirname(dirname(fileURLToPath(import.meta.url))),
46
+ execPath: process.execPath,
47
+ };
48
+ }
package/dist/main.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { Command } from "commander";
2
+ import { VERSION, type Builtin, type Host } from "./context.js";
3
+ import type { CliIO } from "./io.js";
4
+ import type { PoieticTechModule } from "./module.js";
5
+ export { VERSION };
6
+ /** Attach a module as `name`, with the shared output configuration, and run its setup. */
7
+ export declare function registerModule(host: Host, program: Command, name: string, mod: PoieticTechModule, summary: string): Promise<Command>;
8
+ /** The command the arguments name, without parsing them: `x` for `help <x>`. */
9
+ export declare function firstOperand(argv: string[]): string | undefined;
10
+ export declare function run(argv: string[], io: CliIO, builtins?: Builtin[]): Promise<number>;
package/dist/main.js ADDED
@@ -0,0 +1,177 @@
1
+ import { Command, CommanderError, Option } from "commander";
2
+ import { CliError, EXIT_ROWS, renderError, toCliError } from "./errors.js";
3
+ import { createContext, createHost, VERSION } from "./context.js";
4
+ import { agentsBuiltin } from "./modules/agents.js";
5
+ import { authBuiltins } from "./modules/auth.js";
6
+ import { modulesBuiltin } from "./modules/modules.js";
7
+ import { readAllowed } from "./allowlist.js";
8
+ import { discover, discoveryRoots, importModule, stateText } from "./discovery.js";
9
+ import { sanitize } from "./text.js";
10
+ import { colourOn, prescanFormat, resolveGlobals } from "./output.js";
11
+ export { VERSION };
12
+ /** The built-ins `run` registers when none are passed. */
13
+ const DEFAULT_BUILTINS = [...authBuiltins, agentsBuiltin, modulesBuiltin];
14
+ function configure(cmd, io) {
15
+ // Errors are rendered by `run` (outputError is silenced), so the only text commander sends to
16
+ // writeErr is the help it shows for a command named without a subcommand. That help is the
17
+ // answer, not an error, so it goes to stdout and `run` exits 0.
18
+ cmd
19
+ .exitOverride()
20
+ .configureOutput({ writeOut: (s) => io.stdout(sanitize(s)), writeErr: (s) => io.stdout(sanitize(s)), outputError: () => { } });
21
+ for (const sub of cmd.commands)
22
+ configure(sub, io);
23
+ }
24
+ function buildProgram(io, host) {
25
+ const program = new Command("poietic-tech")
26
+ .version(VERSION, "-V, --version")
27
+ .option("--json", "same as --format json")
28
+ .addOption(new Option("--format <format>", "output format").choices(["table", "json", "ndjson", "ids"]))
29
+ .option("-q, --quiet", "print only what was asked for")
30
+ .option("--server <origin>", "the auth server to use")
31
+ .option("--no-wait", "fail instead of waiting for a step-up")
32
+ .option("--no-color", "never colour output");
33
+ program.hook("preAction", (_, action) => {
34
+ const o = action.optsWithGlobals();
35
+ host.globals = resolveGlobals({ ...o, color: o.color !== false, wait: o.wait !== false }, io);
36
+ });
37
+ configure(program, io);
38
+ return program;
39
+ }
40
+ /** `help [topic]`: the root help, `exit-codes`, or the help of a registered command. */
41
+ function addHelp(program, io) {
42
+ program
43
+ .helpCommand(false)
44
+ .command("help [topic]")
45
+ .description("show help, or `help exit-codes` for the exit codes")
46
+ .action((topic) => {
47
+ if (topic === undefined)
48
+ return program.outputHelp();
49
+ if (topic === "exit-codes") {
50
+ for (const r of EXIT_ROWS)
51
+ io.stdout(`${r.exit} ${r.meaning}${r.codes.length ? ` (${r.codes.join(", ")})` : ""}\n`);
52
+ return;
53
+ }
54
+ const target = program.commands.find((c) => c.name() === topic && c.name() !== "help");
55
+ if (!target)
56
+ throw new CliError("bad_usage", `No help topic "${sanitize(topic)}"; try \`poietic-tech --help\`.`);
57
+ target.outputHelp();
58
+ });
59
+ configure(program.commands.at(-1), io);
60
+ }
61
+ /** Attach a module as `name`, with the shared output configuration, and run its setup. */
62
+ export async function registerModule(host, program, name, mod, summary) {
63
+ const cmd = program.command(name).description(summary);
64
+ configure(cmd, host.io);
65
+ await mod.setup(createContext(host, cmd));
66
+ // Subcommands made with `new Command` and added by the module inherit nothing.
67
+ configure(cmd, host.io);
68
+ return cmd;
69
+ }
70
+ /** Global options that take a value, so the value is not mistaken for the command. */
71
+ const VALUED = new Set(["--format", "--server"]);
72
+ /** The command the arguments name, without parsing them: `x` for `help <x>`. */
73
+ export function firstOperand(argv) {
74
+ const operands = [];
75
+ for (let i = 0; i < argv.length && operands.length < 2; i++) {
76
+ const a = argv[i];
77
+ if (a === "--") {
78
+ operands.push(...argv.slice(i + 1, i + 3));
79
+ break;
80
+ }
81
+ if (VALUED.has(a))
82
+ i++;
83
+ else if (!a.startsWith("-")) {
84
+ operands.push(a);
85
+ if (operands[0] !== "help")
86
+ break;
87
+ }
88
+ }
89
+ return operands[0] === "help" ? (operands[1] ?? "help") : operands[0];
90
+ }
91
+ /**
92
+ * Find the modules, import and register the one the arguments name, and give
93
+ * every other available one a placeholder so --help lists it from its summary.
94
+ */
95
+ async function addModules(host, program, argv, builtinNames) {
96
+ const { io } = host;
97
+ const operand = firstOperand(argv);
98
+ // A built-in, or the built-in help topic `exit-codes`: no module is looked for, let alone imported.
99
+ if (operand === "exit-codes" || (operand !== undefined && operand !== "help" && builtinNames.includes(operand)))
100
+ return;
101
+ let allowed = [];
102
+ let held = null;
103
+ try {
104
+ allowed = await readAllowed(io.env);
105
+ }
106
+ catch (error) {
107
+ held = error;
108
+ }
109
+ const roots = discoveryRoots(io.hostDir, io.execPath);
110
+ const found = await discover(roots, allowed, { builtins: builtinNames });
111
+ for (const d of found) {
112
+ if (d.state.kind !== "available" || d.command === null)
113
+ continue;
114
+ if (d.command !== operand) {
115
+ configure(program.command(d.command).description(d.summary), io);
116
+ continue;
117
+ }
118
+ const mod = await importModule(d);
119
+ try {
120
+ await registerModule(host, program, d.command, mod, d.summary);
121
+ }
122
+ catch {
123
+ throw new CliError("module_failed", `The module ${d.name} failed to load.`);
124
+ }
125
+ }
126
+ // A broken allow file matters only to a command it could have named.
127
+ const scoped = found.some((d) => d.command === operand && d.name.startsWith("@poietic-tech/"));
128
+ if (held !== null && operand !== undefined && operand !== "help" && !scoped)
129
+ throw held;
130
+ if (operand === undefined || operand === "help" || found.some((d) => d.command === operand && d.state.kind === "available"))
131
+ return;
132
+ // Say why a module's command does not run, rather than "unknown command". Only on this
133
+ // failing path are other packages' package.json files read, to find a not-allowed one.
134
+ const blocked = found.find((d) => d.command === operand) ??
135
+ (held === null
136
+ ? (await discover(roots, allowed, { builtins: builtinNames, unscoped: true })).find((d) => d.command === operand)
137
+ : undefined);
138
+ if (blocked)
139
+ throw notRunnable(blocked);
140
+ }
141
+ function notRunnable(d) {
142
+ if (d.state.kind === "not-allowed") {
143
+ return new CliError("bad_usage", `"${d.command}" comes from ${d.name}, which is not allowed; allow it with \`poietic-tech modules allow ${d.name}\`.`);
144
+ }
145
+ return new CliError("bad_usage", `"${d.command}" comes from ${d.name}, which does not run (${stateText(d.state)}); see \`poietic-tech modules list\`.`);
146
+ }
147
+ export async function run(argv, io, builtins = DEFAULT_BUILTINS) {
148
+ const host = createHost(io);
149
+ const fail = (e) => {
150
+ const format = host.globals?.format ?? prescanFormat(argv, io);
151
+ const noColor = argv.slice(0, argv.indexOf("--") < 0 ? argv.length : argv.indexOf("--")).includes("--no-color");
152
+ const color = host.globals?.color ?? colourOn(!noColor, format, io);
153
+ io.stderr(renderError(e, format !== "table", color));
154
+ return e.exitCode;
155
+ };
156
+ if (io.platform === "win32") {
157
+ return fail(new CliError("unsupported_platform", "poietic-tech does not run on Windows yet."));
158
+ }
159
+ host.builtins = builtins.map(([name]) => name);
160
+ try {
161
+ const program = buildProgram(io, host);
162
+ for (const [name, summary, make] of builtins)
163
+ await registerModule(host, program, name, make(host), summary);
164
+ addHelp(program, io);
165
+ await addModules(host, program, argv, builtins.map(([name]) => name));
166
+ await program.parseAsync(argv, { from: "user" });
167
+ return 0;
168
+ }
169
+ catch (error) {
170
+ if (error instanceof CommanderError) {
171
+ if (error.exitCode === 0 || error.code === "commander.help")
172
+ return 0;
173
+ return fail(new CliError("bad_usage", error.message.replace(/^error: /, "")));
174
+ }
175
+ return fail(toCliError(error, io.signal));
176
+ }
177
+ }
@@ -0,0 +1,48 @@
1
+ /**
2
+ * @poietic-tech/cli/module: the module contract, API version 1. The only part
3
+ * of @poietic-tech/cli a module may rely on; everything else is private.
4
+ */
5
+ import type { Command } from "commander";
6
+ import type { Caller } from "@poietic-tech/auth/node";
7
+ import type { Column } from "./output.js";
8
+ export type { Caller } from "@poietic-tech/auth/node";
9
+ export type { Column } from "./output.js";
10
+ export declare const MODULE_API = 1;
11
+ /** The "poietic-tech" key of a module's package.json. */
12
+ export interface ModuleManifest {
13
+ command: string;
14
+ entry: string;
15
+ api: number;
16
+ summary: string;
17
+ }
18
+ /** Results to stdout and messages to stderr, honouring --json, --format and -q. */
19
+ export interface ModuleOutput {
20
+ item<T>(obj: T, columns?: Column<T>[]): void;
21
+ list<T>(rows: T[], opts: {
22
+ columns: Column<T>[];
23
+ id?: (row: T) => string;
24
+ }): void;
25
+ message(text: string): void;
26
+ warn(text: string): void;
27
+ secret(value: string, warning: string, extra?: Record<string, unknown>): void;
28
+ }
29
+ export interface ModuleContext {
30
+ readonly program: Command;
31
+ caller(): Promise<Caller>;
32
+ readonly output: ModuleOutput;
33
+ fail(code: string, message: string, opts?: {
34
+ status?: number;
35
+ }): never;
36
+ stepUp(): Promise<void>;
37
+ readonly server: string;
38
+ readonly env: Readonly<Record<string, string | undefined>>;
39
+ readonly version: {
40
+ readonly host: string;
41
+ readonly api: readonly number[];
42
+ };
43
+ }
44
+ export interface PoieticTechModule {
45
+ readonly api: 1;
46
+ readonly setup: (ctx: ModuleContext) => void | Promise<void>;
47
+ }
48
+ export declare function defineModule(setup: PoieticTechModule["setup"]): PoieticTechModule;
package/dist/module.js ADDED
@@ -0,0 +1,4 @@
1
+ export const MODULE_API = 1;
2
+ export function defineModule(setup) {
3
+ return Object.freeze({ api: 1, setup });
4
+ }
@@ -0,0 +1,48 @@
1
+ import type { Builtin } from "../context.js";
2
+ import { type Column, type ModuleContext } from "../module.js";
3
+ /** The server's AGENT_PATH segment. */
4
+ export declare const ID: RegExp;
5
+ export interface AgentRow {
6
+ id: string;
7
+ name: string;
8
+ owner: string;
9
+ created_by: string | null;
10
+ builder: boolean;
11
+ delegator: boolean;
12
+ created_at: number;
13
+ level?: string;
14
+ act_as_allowed?: boolean;
15
+ live_tokens?: number;
16
+ }
17
+ export interface TokenRow {
18
+ id: string;
19
+ label: string | null;
20
+ created_at: number;
21
+ expires_at: number | null;
22
+ last_used_at: number | null;
23
+ state: string;
24
+ created_via?: string | null;
25
+ created_via_label?: string | null;
26
+ }
27
+ /**
28
+ * Calls the /agents API as the person (or the builder named by POIETIC_IDENTITY). Returns the JSON of a 2xx.
29
+ * `signal` (the run's Ctrl-C) aborts the request in flight.
30
+ * On `403 reauth_required` it steps up (`ctx.stepUp()`, which fails at once under --no-wait) and
31
+ * sends once more; a second `reauth_required` is the error. An aborted `signal` never resends.
32
+ */
33
+ export declare function agentsCall(ctx: ModuleContext, method: string, path: string, body?: unknown, signal?: AbortSignal): Promise<any>;
34
+ /** Only the person may change an agent: the builder view has no PATCH. Fails before any request. */
35
+ export declare function personOnly(ctx: ModuleContext, what: string): Promise<void>;
36
+ export declare function listAgents(ctx: ModuleContext, signal?: AbortSignal): Promise<AgentRow[]>;
37
+ /** `ref` is a name (as the server resolves POIETIC_IDENTITY) or `id:<uuid>`. */
38
+ export declare function resolveAgent(ctx: ModuleContext, ref: string, opts?: {
39
+ list?: boolean;
40
+ signal?: AbortSignal;
41
+ }): Promise<{
42
+ id: string;
43
+ agent?: AgentRow;
44
+ }>;
45
+ export declare function levelOf(a: AgentRow): "none" | "builder" | "delegator";
46
+ export declare function agentColumns(all: readonly AgentRow[]): Column<AgentRow>[];
47
+ export declare const TOKEN_COLUMNS: Column<TokenRow>[];
48
+ export declare const agentsBuiltin: Builtin;