@anteros/core 0.0.41 → 0.0.44

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 (133) hide show
  1. package/database/audit.ts +9 -59
  2. package/database/collection.ts +3 -1
  3. package/database/deleteLog.ts +67 -3
  4. package/database/file.ts +4 -3
  5. package/database/mongodbadapter.ts +103 -42
  6. package/database/replication.ts +724 -58
  7. package/database/ttl.ts +68 -0
  8. package/database/workflow.ts +466 -240
  9. package/index.ts +6 -0
  10. package/lib/agent.ts +1567 -0
  11. package/lib/agentMemory.ts +689 -0
  12. package/lib/agents.ts +180 -0
  13. package/lib/ai.ts +0 -0
  14. package/lib/api.ts +246 -0
  15. package/lib/attachment.ts +351 -0
  16. package/lib/auditResult.ts +13 -0
  17. package/lib/define.ts +43 -1
  18. package/lib/endpoints.ts +80 -0
  19. package/lib/files.ts +2 -1
  20. package/lib/jsonSchema.ts +183 -0
  21. package/lib/lifecycle.ts +7 -1
  22. package/lib/load.ts +17 -0
  23. package/lib/mcp.ts +3 -2
  24. package/lib/mcpClient.ts +128 -0
  25. package/lib/messages.ts +299 -0
  26. package/lib/providers.ts +970 -0
  27. package/lib/routes.ts +14 -1
  28. package/lib/scriptWorker.ts +4 -1
  29. package/lib/scripts.ts +6 -1
  30. package/lib/skills.ts +481 -0
  31. package/lib/sockets.ts +3 -1
  32. package/lib/workflow.ts +209 -37
  33. package/lib/zip.ts +109 -0
  34. package/package.json +1 -1
  35. package/server/access.ts +96 -0
  36. package/server/agents.ts +472 -0
  37. package/server/api.ts +80 -65
  38. package/server/boot.ts +11 -0
  39. package/server/hono.ts +8 -3
  40. package/server/mcp.ts +17 -63
  41. package/server/routes.ts +19 -24
  42. package/tests/agent-mcp.test.ts +116 -0
  43. package/tests/agent-memory.test.ts +617 -0
  44. package/tests/agent-provider.test.ts +288 -0
  45. package/tests/agent-runtime.test.ts +369 -0
  46. package/tests/agent-skills.test.ts +177 -0
  47. package/tests/agent-summarize.test.ts +124 -0
  48. package/tests/agent-working-memory.test.ts +404 -0
  49. package/tests/agent.test.ts +1241 -0
  50. package/tests/agents-api.test.ts +593 -0
  51. package/tests/api-private-fields.test.ts +196 -0
  52. package/tests/api.test.ts +51 -0
  53. package/tests/attachment.test.ts +241 -0
  54. package/tests/audit-operations.test.ts +1 -1
  55. package/tests/audit-redact.test.ts +29 -3
  56. package/tests/audit-run-action.test.ts +1 -1
  57. package/tests/endpoints.test.ts +80 -0
  58. package/tests/file-collection-replication.test.ts +2 -0
  59. package/tests/fixtures/agent-tenant/agents/support.agent.ts +9 -0
  60. package/tests/fixtures/agent-tenant/agents/weather.agent.ts +21 -0
  61. package/tests/fixtures/agents-api/agents/assistant.agent.ts +46 -0
  62. package/tests/fixtures/agents-api/agents/plain.agent.ts +23 -0
  63. package/tests/fixtures/agents-api/agents/private.agent.ts +15 -0
  64. package/tests/fixtures/agents-api/collections/notes.model.ts +26 -0
  65. package/tests/fixtures/agents-api/routes/probe.route.ts +21 -0
  66. package/tests/fixtures/agents-api/services/probe.service.ts +19 -0
  67. package/tests/fixtures/attachment/data.csv +2 -0
  68. package/tests/fixtures/fake-provider.ts +128 -0
  69. package/tests/fixtures/repl-only/app.ts +3 -0
  70. package/tests/fixtures/replication-meta/agents/chat.agent.ts +16 -0
  71. package/tests/fixtures/replication-meta/collections/counters.model.ts +13 -0
  72. package/tests/fixtures/replication-meta/collections/orders.model.ts +9 -0
  73. package/tests/fixtures/replication-meta/workflows/run.workflow.ts +8 -0
  74. package/tests/fixtures/skills-tenant/agents/docs.agent.ts +23 -0
  75. package/tests/fixtures/skills-tenant/agents/incomplete.agent.ts +14 -0
  76. package/tests/fixtures/skills-tenant/broken/bad-name.md +6 -0
  77. package/tests/fixtures/skills-tenant/broken/dup-a.md +6 -0
  78. package/tests/fixtures/skills-tenant/broken/dup-b.md +6 -0
  79. package/tests/fixtures/skills-tenant/broken/empty-body.md +4 -0
  80. package/tests/fixtures/skills-tenant/broken/long-compat.md +7 -0
  81. package/tests/fixtures/skills-tenant/broken/long-description.md +6 -0
  82. package/tests/fixtures/skills-tenant/broken/mismatch/SKILL.md +6 -0
  83. package/tests/fixtures/skills-tenant/broken/no-description.md +5 -0
  84. package/tests/fixtures/skills-tenant/broken/no-name.md +5 -0
  85. package/tests/fixtures/skills-tenant/broken/plain.md +4 -0
  86. package/tests/fixtures/skills-tenant/broken/unknown-key.md +9 -0
  87. package/tests/fixtures/skills-tenant/docs/pdf-forms.md +18 -0
  88. package/tests/fixtures/skills-tenant/guide/ml.md +9 -0
  89. package/tests/fixtures/skills-tenant/guides/pdf/SKILL.md +9 -0
  90. package/tests/fixtures/skills-tenant/runbooks/incident.md +9 -0
  91. package/tests/fixtures/skills-tenant/runbooks/notes.txt +2 -0
  92. package/tests/fixtures/vars-tenant/vars/private.var.ts +4 -1
  93. package/tests/fixtures/workflow-engine/workflows/failing.workflow.ts +34 -0
  94. package/tests/fixtures/workflow-engine/workflows/flaky.workflow.ts +23 -0
  95. package/tests/fixtures/workflow-engine/workflows/hang.workflow.ts +17 -0
  96. package/tests/fixtures/workflow-engine/workflows/simple.workflow.ts +19 -0
  97. package/tests/fixtures/workflow-engine/workflows/slow.workflow.ts +23 -0
  98. package/tests/fixtures/workflow-engine/workflows/versioned.workflow.ts +19 -0
  99. package/tests/fixtures/workflow-tenant/workflows/broken.workflow.ts +3 -0
  100. package/tests/fixtures/workflow-tenant/workflows/disabled.workflow.ts +8 -0
  101. package/tests/fixtures/workflow-tenant/workflows/invoice.workflow.ts +21 -0
  102. package/tests/fixtures/workflow-tenant/workflows/no-id.workflow.ts +7 -0
  103. package/tests/fixtures/workflow-tenant/workflows/order-duplicate.workflow.ts +13 -0
  104. package/tests/fixtures/workflow-tenant/workflows/order.workflow.ts +17 -0
  105. package/tests/hono.test.ts +6 -3
  106. package/tests/locks.test.ts +120 -0
  107. package/tests/mcp.test.ts +11 -11
  108. package/tests/messages.test.ts +258 -0
  109. package/tests/replication-change-stream.test.ts +240 -0
  110. package/tests/replication-control.test.ts +4 -2
  111. package/tests/replication-initial-sync.test.ts +2 -0
  112. package/tests/replication-meta.test.ts +246 -0
  113. package/tests/replication.test.ts +2 -0
  114. package/tests/upload.test.ts +44 -12
  115. package/tests/vars-api.test.ts +1 -1
  116. package/tests/vars-replication.test.ts +5 -2
  117. package/tests/workflow-engine.test.ts +340 -0
  118. package/tests/workflow-retention.test.ts +103 -0
  119. package/tests/workflow.test.ts +186 -0
  120. package/types/agent.d.ts +1067 -0
  121. package/types/config.d.ts +16 -0
  122. package/types/file.d.ts +7 -7
  123. package/types/hook.d.ts +6 -0
  124. package/types/lifecycle.d.ts +6 -0
  125. package/types/mcp.d.ts +11 -1
  126. package/types/replication.d.ts +70 -4
  127. package/types/route.d.ts +7 -1
  128. package/types/scripts.d.ts +6 -0
  129. package/types/service.d.ts +7 -1
  130. package/types/tenant.d.ts +13 -0
  131. package/types/vars.d.ts +2 -2
  132. package/types/websocket.d.ts +6 -0
  133. package/types/workflow.d.ts +50 -3
package/lib/agents.ts ADDED
@@ -0,0 +1,180 @@
1
+ import { Glob } from "bun";
2
+ import path from "path";
3
+ import fs from "fs/promises";
4
+ import { cfg } from "../server/config";
5
+ import { Agent } from "./agent";
6
+ import { InMemoryAgentMemory, MongoAgentMemory, RedisAgentMemory, summarize } from "./agentMemory";
7
+ import { importDefinition } from "./load";
8
+ import { resolveSkills } from "./skills";
9
+ import { closeMcpClients } from "./mcpClient";
10
+ import { logger } from "../utils/logger";
11
+ import type { useRest } from "../database/rest";
12
+ import type { AgentDefinition, TenantAgents } from "../types/agent";
13
+
14
+ /**
15
+ * Agent loader — `{tenant.dir}/agents/**\/*.agent.ts`.
16
+ *
17
+ * Each definition is instantiated once per tenant and kept in a process-wide
18
+ * registry keyed by `tenant:agent`. `agents.get('weather')` resolves it at
19
+ * runtime, binding the *calling* `rest` so tools run with the right scope.
20
+ */
21
+
22
+ const registry = new Map<string, Agent>();
23
+ const definitions: AgentDefinition[] = [];
24
+
25
+ async function syncAgents(): Promise<void> {
26
+ registry.clear();
27
+ definitions.length = 0;
28
+ // A re-scan reconnects the remote MCP servers (they are cached per URL)
29
+ await closeMcpClients().catch(() => {});
30
+ const memories: Array<{ _tenant_: string; collection: string }> = [];
31
+
32
+ for (const tenant of cfg.tenants ?? []) {
33
+ const agentsPath = path.join(process.cwd(), tenant.dir, "agents");
34
+ if (!(await fs.exists(agentsPath))) continue;
35
+ if (!(await fs.stat(agentsPath)).isDirectory()) continue;
36
+
37
+ const glob = new Glob(path.join(agentsPath, "**/*.agent.ts"));
38
+ for await (const file of glob.scan(".")) {
39
+ const module = await importDefinition(file, "Agent");
40
+ const definition: AgentDefinition | undefined = module?.default;
41
+ if (!definition?._isAgent_) continue;
42
+ if (definition.enabled === false) continue;
43
+
44
+ // `id` and `description` are required — the `Agent` constructor refuses a
45
+ // definition without them, and a broken file never hides the others
46
+ const id = definition.id!;
47
+
48
+ const key = `${tenant.id}:${id}`;
49
+ const existing = registry.get(key);
50
+ if (existing) {
51
+ logger.warn(`Duplicate agent id '${id}' for tenant '${tenant.id}' — '${file}' replaces the previous definition`);
52
+ }
53
+
54
+ try {
55
+ // `skills` are read **once**, here: a run injects the documents into
56
+ // its prompt, it never touches the filesystem
57
+ const baseDir = path.join(process.cwd(), tenant.dir);
58
+ const skills = await resolveSkills(definition.skills, { baseDir, agentId: id });
59
+
60
+ // A referenced sub-agent brings its own declaration: its `skills` are
61
+ // resolved here too, so delegating is the same as running it directly
62
+ // (and still never reads a file at run time)
63
+ for (const referenced of definition.agents
64
+ ? (Array.isArray(definition.agents) ? definition.agents : [definition.agents])
65
+ : []) {
66
+ if (referenced && !referenced._skills_?.length) {
67
+ referenced._skills_ = await resolveSkills(referenced.skills, { baseDir, agentId: referenced.id });
68
+ }
69
+ }
70
+
71
+ const instance = new Agent({ ...definition, id, _skills_: skills }, { tenant: tenant.id });
72
+ registry.set(key, instance);
73
+ definitions.push(instance.getConfig());
74
+
75
+ // A store says which collections it writes to (a Mongo memory does):
76
+ // published on `cfg` so the replication engine copies them, exactly
77
+ // like a declared collection — the tenant chooses the name.
78
+ for (const collection of instance.getMemory()?.collections?.() ?? []) {
79
+ if (!memories.some((m) => m._tenant_ === tenant.id && m.collection === collection)) {
80
+ memories.push({ _tenant_: tenant.id, collection });
81
+ }
82
+ }
83
+ } catch (err: any) {
84
+ console.error(`Failed to load agent '${id}' of tenant '${tenant.id}': ${err?.message}`);
85
+ }
86
+ }
87
+ }
88
+
89
+ cfg.agents = definitions;
90
+ cfg.agentMemories = memories;
91
+ }
92
+
93
+ /**
94
+ * The agents declared by a tenant, ready to run.
95
+ *
96
+ * When a `rest` is given (always the case through the `agents` member of a
97
+ * context), every call
98
+ * returns a **copy** bound to it. The registry instance is shared by every
99
+ * caller — binding it in place would let one request's client (and its Mongo
100
+ * session, hence its transaction) leak into another request running at the same
101
+ * time. A copy shares the declaration (instructions, tools, memory), so nothing
102
+ * else changes.
103
+ */
104
+ function createAgents(tenantId: string, rest?: InstanceType<typeof useRest> | any): TenantAgents {
105
+ const prefix = `${tenantId}:`;
106
+ const bind = (agent: Agent): Agent => rest
107
+ ? new Agent(agent.getConfig(), { rest, tenant: agent.getTenant() })
108
+ : agent;
109
+ const entries = (): Array<[string, Agent]> =>
110
+ [...registry.entries()].filter(([key]) => key.startsWith(prefix));
111
+
112
+ return {
113
+ get: (id) => {
114
+ const agent = registry.get(`${prefix}${id}`);
115
+ return agent ? bind(agent) : undefined;
116
+ },
117
+ has: (id) => registry.has(`${prefix}${id}`),
118
+ ids: () => entries().map(([key]) => key.slice(prefix.length)),
119
+ list: () => entries().map(([, agent]) => bind(agent)),
120
+ reload: syncAgents,
121
+ };
122
+ }
123
+
124
+ /** Boot banner counters. */
125
+ function agentsStats(): { total: number; tenants: string[] } {
126
+ const tenants = new Set([...registry.keys()].map((key) => key.slice(0, key.indexOf(":"))));
127
+ return { total: registry.size, tenants: [...tenants] };
128
+ }
129
+
130
+ /**
131
+ * Process-wide agent registry, importable from **anywhere** — including a place
132
+ * with no `rest` at hand: module scope, a global middleware, a `Bun.cron`, a
133
+ * `Bun.serve` of your own.
134
+ *
135
+ * ```ts
136
+ * import { agents } from '@anteros/core'
137
+ *
138
+ * const support = agents.get('v1', 'support')
139
+ * await support.generate('Where is my order?')
140
+ * ```
141
+ *
142
+ * Prefer the `agents` member of a context whenever there is one (a script, a
143
+ * service action, a hook, a socket, a lifecycle hook, a workflow step, a route,
144
+ * an MCP tool): the tenant is implicit and the **calling** client is bound, so the
145
+ * agent's tools run with the right scope. Without a `rest`, pass one as the third
146
+ * argument (`agents.get(tenantId, id, rest)`) or the tools run without a database
147
+ * client.
148
+ */
149
+ const agents = {
150
+ /** The agent, bound to `rest` when provided — `undefined` if it does not exist. */
151
+ get: (tenantId: string, id: string, rest?: InstanceType<typeof useRest>): Agent | undefined =>
152
+ createAgents(tenantId, rest).get(id),
153
+ has: (tenantId: string, id: string): boolean => registry.has(`${tenantId}:${id}`),
154
+ /** Declared agent ids of a tenant. */
155
+ ids: (tenantId: string): string[] => createAgents(tenantId).ids(),
156
+ list: (tenantId: string, rest?: InstanceType<typeof useRest>): Agent[] => createAgents(tenantId, rest).list(),
157
+ /** Tenant-scoped registry — the same object a context receives as `agents`. */
158
+ use: createAgents,
159
+ /** Re-scan `{tenant.dir}/agents` and rebuild the registry. */
160
+ reload: syncAgents,
161
+ /** `{ total, tenants }` — what the boot banner prints. */
162
+ stats: agentsStats,
163
+ memory: {
164
+ InMemory: InMemoryAgentMemory,
165
+ Mongo: MongoAgentMemory,
166
+ Redis: RedisAgentMemory,
167
+ // A `load` processor, not a store: it compacts a thread with **your** model
168
+ // and prompt. Every store above is a backend; this one shapes the prompt.
169
+ summarize,
170
+ },
171
+ };
172
+
173
+ /** Mongo collections used as agent memory by a tenant — published by the loader. */
174
+ function agentMemoryCollections(tenantId: string): string[] {
175
+ return (cfg.agentMemories ?? [])
176
+ .filter((memory) => memory._tenant_ === tenantId)
177
+ .map((memory) => memory.collection);
178
+ }
179
+
180
+ export { syncAgents, createAgents, agentsStats, agents, agentMemoryCollections, InMemoryAgentMemory, MongoAgentMemory, RedisAgentMemory };
package/lib/ai.ts ADDED
File without changes
package/lib/api.ts ADDED
@@ -0,0 +1,246 @@
1
+ import { createAgents } from "./agents";
2
+ import { endpoints } from "./endpoints";
3
+ import { handleDelete } from "./files";
4
+ import type { useRest } from "../database/rest";
5
+ import type { Agent } from "./agent";
6
+ import type { FindCallOptions, FindOptions, findOneOptions } from "../types/mongo";
7
+ import type { updateApiOptions } from "../types/api";
8
+ import type { TenantVars } from "../types/vars";
9
+ import type { BulkUpdateOperation } from "../types/rest";
10
+ import type {
11
+ AnyBulkWriteOperation,
12
+ BulkWriteOptions,
13
+ BulkWriteResult,
14
+ ChangeStream,
15
+ ChangeStreamOptions,
16
+ DeleteResult,
17
+ Document,
18
+ UpdateFilter,
19
+ UpdateResult,
20
+ } from "mongodb";
21
+
22
+ type Rest = InstanceType<typeof useRest>;
23
+
24
+ /**
25
+ * The in-process facade — an `api` member in every context, next to `rest`,
26
+ * `io` and `agents`.
27
+ *
28
+ * It is **the same thing as `rest`, grouped the way the SDK and the HTTP API are
29
+ * grouped**: one namespace per family, and a **bound client** for a collection.
30
+ * Nothing is re-implemented — every call lands on the very same `rest` method the
31
+ * context already had, so audit, hooks, access rules, the session and the
32
+ * transaction are those of the caller.
33
+ *
34
+ * ```ts
35
+ * export default define.Hook(async ({ api, rest }) => {
36
+ * // Bound to one collection — the slug is stated once
37
+ * const orders = api.collection('orders');
38
+ * const [order] = await orders.find({ $match: { status: 'paid' }, $limit: 1 });
39
+ * await orders.updateOne(order._id, { $set: { status: 'shipped' } });
40
+ *
41
+ * // The families
42
+ * await api.vars.set('config', 'lastRun', new Date().toISOString());
43
+ * await api.service('analytics').run('refresh', { orderId: order._id });
44
+ * const url = api.files.url('invoices', 'f1.pdf', { width: 800 });
45
+ * await api.agent('triage')?.generate(`Classify: ${order.ref}`);
46
+ *
47
+ * // …and `rest` stays exactly what it was
48
+ * await rest.aggregate('orders', [{ $count: 'total' }]);
49
+ * });
50
+ * ```
51
+ *
52
+ * Naming rule, shared with the SDK: **singular = a client bound to one resource**
53
+ * (`api.collection(slug)`, `api.agent(id)`), **plural = the family's operations**
54
+ * (`api.vars`, `api.files`).
55
+ */
56
+
57
+ /** One collection, bound to the `rest` of the context. */
58
+ class ApiCollection<T = any> {
59
+ #rest: Rest;
60
+ #slug: string;
61
+
62
+ constructor(rest: Rest, slug: string) {
63
+ this.#rest = rest;
64
+ this.#slug = slug;
65
+ }
66
+
67
+ /** The slug this client is bound to. */
68
+ getSlug(): string {
69
+ return this.#slug;
70
+ }
71
+
72
+ // ── Reads ────────────────────────────────────────────────────────────
73
+
74
+ find(params: FindOptions = {}, options: FindCallOptions = {}): Promise<Document[]> {
75
+ return this.#rest.find(this.#slug, params, options);
76
+ }
77
+
78
+ findOne(_id: string, params?: findOneOptions): Promise<any> {
79
+ return this.#rest.findOne(this.#slug, _id, params);
80
+ }
81
+
82
+ aggregate<T = Document>(pipeline: any[]): Promise<T[]> {
83
+ return this.#rest.aggregate(this.#slug, pipeline) as Promise<T[]>;
84
+ }
85
+
86
+ countDocuments(query: any = {}): Promise<number> {
87
+ return this.#rest.countDocuments(this.#slug, query);
88
+ }
89
+
90
+ /** The collection's change stream — the caller consumes it. */
91
+ watch(pipeline: any[], options: ChangeStreamOptions): Promise<ChangeStream> {
92
+ return this.#rest.watch(this.#slug, pipeline, options);
93
+ }
94
+
95
+ // ── Writes ───────────────────────────────────────────────────────────
96
+
97
+ insertOne<TBody>(data: TBody): Promise<TBody & { _id: string }> {
98
+ return this.#rest.insertOne(this.#slug, data);
99
+ }
100
+
101
+ insertMany<TBody>(data: TBody[]): Promise<(TBody & { _id: string })[]> {
102
+ return this.#rest.insertMany(this.#slug, data);
103
+ }
104
+
105
+ updateOne(_id: string, update: UpdateFilter<any>): Promise<UpdateResult & { previous?: any }> {
106
+ return this.#rest.updateOne(this.#slug, _id, update) as Promise<UpdateResult & { previous?: any }>;
107
+ }
108
+
109
+ findOneAndUpdate(
110
+ filter: Document,
111
+ update: UpdateFilter<any>,
112
+ options?: updateApiOptions,
113
+ ): Promise<Document | null> {
114
+ return this.#rest.findOneAndUpdate(this.#slug, filter, update, options);
115
+ }
116
+
117
+ updateMany(_ids: string[], update: UpdateFilter<any>): Promise<UpdateResult> {
118
+ return this.#rest.updateMany(this.#slug, _ids, update);
119
+ }
120
+
121
+ deleteOne(_id: string): Promise<any> {
122
+ return this.#rest.deleteOne(this.#slug, _id);
123
+ }
124
+
125
+ deleteMany(_ids: string[]): Promise<DeleteResult> {
126
+ return this.#rest.deleteMany(this.#slug, _ids);
127
+ }
128
+
129
+ bulkWrite(operations: AnyBulkWriteOperation[], options?: BulkWriteOptions): Promise<BulkWriteResult> {
130
+ return this.#rest.bulkWrite(this.#slug, operations, options);
131
+ }
132
+
133
+ bulkUpdate(operations: BulkUpdateOperation[], options?: BulkWriteOptions): Promise<BulkWriteResult> {
134
+ return this.#rest.bulkUpdate(this.#slug, operations, options);
135
+ }
136
+
137
+ // ── Maintenance & custom actions ─────────────────────────────────────
138
+
139
+ dropCollection(): Promise<any> {
140
+ return this.#rest.dropCollection(this.#slug);
141
+ }
142
+
143
+ dropIndex(index: string): Promise<any> {
144
+ return this.#rest.dropIndex(this.#slug, index);
145
+ }
146
+
147
+ dropIndexes(): Promise<any> {
148
+ return this.#rest.dropIndexes(this.#slug);
149
+ }
150
+
151
+ /** A custom action declared on the collection (`define.Action` / `actions`). */
152
+ runAction<T = any>(action: string, data?: any): Promise<T> {
153
+ return this.#rest.runAction<T>(this.#slug, action, data);
154
+ }
155
+ }
156
+
157
+ /** One service of the tenant, bound to its name — `api.service('analytics')`. */
158
+ class ApiService {
159
+ #rest: Rest;
160
+ #name: string;
161
+
162
+ constructor(rest: Rest, name: string) {
163
+ this.#rest = rest;
164
+ this.#name = name;
165
+ }
166
+
167
+ /** The service name this client is bound to. */
168
+ getName(): string {
169
+ return this.#name;
170
+ }
171
+
172
+ /** Run one action of the service — `data` is what the handler reads as `body.data`. */
173
+ run<T = any>(action: string, data?: any): Promise<T> {
174
+ return this.#rest.runService<T>(this.#name, action, data);
175
+ }
176
+ }
177
+
178
+ /** The `api` member of a context — see the class doc for the convention. */
179
+ type Api = {
180
+ /** One collection, bound: `api.collection('orders').find(...)`. */
181
+ collection<T = any>(slug: string): ApiCollection<T>;
182
+ /** The tenant's variables — the same object as `rest.vars`. */
183
+ vars: TenantVars;
184
+ /** One service, bound: `api.service('analytics').run('monthly', data)`. */
185
+ service<T = any>(name: string): ApiService & { run<R = T>(action: string, data?: any): Promise<R> };
186
+ /** File collections — the URL of a stored file, and its deletion. */
187
+ files: {
188
+ url(collection: string, filename: string, transform?: {
189
+ width?: number;
190
+ height?: number;
191
+ format?: "webp" | "jpeg" | "png" | "avif";
192
+ quality?: number;
193
+ }): string;
194
+ delete(collection: string, fileId: string): Promise<{ message: string; ok: boolean }>;
195
+ };
196
+ /** One agent of the tenant, bound to the same `rest` — `undefined` if unknown. */
197
+ agent(id: string): Agent | undefined;
198
+ };
199
+
200
+ /**
201
+ * Build the `api` facade of a context. Everything delegates to `rest`, so a hook,
202
+ * a script or a workflow step gets the same client it already had — only the shape
203
+ * changes.
204
+ */
205
+ function createApi(rest: Rest): Api {
206
+ const tenant = rest.tenant_id;
207
+ const registry = createAgents(tenant, rest);
208
+
209
+ return {
210
+ collection: <T = any>(slug: string) => new ApiCollection<T>(rest, slug),
211
+ // `rest.vars` returns a fresh bound client on each access — keep that
212
+ get vars() {
213
+ return rest.vars;
214
+ },
215
+ service: (name: string) => new ApiService(rest, name),
216
+ files: {
217
+ /** `/api/<tenant>/files/<collection>/<file>` (+ `w`, `h`, `format`, `q`). */
218
+ url(collection: string, filename: string, transform?: {
219
+ width?: number;
220
+ height?: number;
221
+ format?: "webp" | "jpeg" | "png" | "avif";
222
+ quality?: number;
223
+ }): string {
224
+ const base = endpoints.file(tenant, collection, filename);
225
+ if (!transform) return base;
226
+
227
+ const params = new URLSearchParams();
228
+ if (transform.width) params.set("w", String(transform.width));
229
+ if (transform.height) params.set("h", String(transform.height));
230
+ if (transform.format) params.set("format", transform.format);
231
+ if (transform.quality) params.set("q", String(transform.quality));
232
+
233
+ const query = params.toString();
234
+ return query ? `${base}?${query}` : base;
235
+ },
236
+ /** Removes the stored binary **and** the document (the route does the same). */
237
+ delete(collection: string, fileId: string) {
238
+ return handleDelete(tenant, collection, fileId).then(() => ({ message: "File deleted", ok: true }));
239
+ },
240
+ },
241
+ agent: (id: string) => registry.get(id),
242
+ } as Api;
243
+ }
244
+
245
+ export { ApiCollection, ApiService, createApi };
246
+ export type { Api };