@zerotal/arch 1.7.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 (160) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/LICENSE +21 -0
  3. package/README.md +67 -0
  4. package/api-surface.md +283 -0
  5. package/docs/about.md +676 -0
  6. package/docs/admin/actions.md +183 -0
  7. package/docs/admin/auth.md +89 -0
  8. package/docs/admin/dashboard.md +155 -0
  9. package/docs/admin/extending-ui.md +229 -0
  10. package/docs/admin/extending.md +205 -0
  11. package/docs/admin/forms.md +155 -0
  12. package/docs/admin/index.md +144 -0
  13. package/docs/admin/operations.md +224 -0
  14. package/docs/admin/references.md +174 -0
  15. package/docs/admin/resources.md +132 -0
  16. package/docs/admin/structure.md +158 -0
  17. package/docs/admin/tables.md +185 -0
  18. package/docs/admin/testing.md +138 -0
  19. package/docs/ai.md +373 -0
  20. package/docs/application.md +435 -0
  21. package/docs/arch.md +263 -0
  22. package/docs/assets.md +755 -0
  23. package/docs/audit.md +367 -0
  24. package/docs/authentication.md +1000 -0
  25. package/docs/authorization.md +529 -0
  26. package/docs/broadcasting/channels.md +170 -0
  27. package/docs/broadcasting/client.md +108 -0
  28. package/docs/broadcasting/events.md +190 -0
  29. package/docs/broadcasting/index.md +163 -0
  30. package/docs/broadcasting/references.md +84 -0
  31. package/docs/broadcasting/testing.md +132 -0
  32. package/docs/cache.md +387 -0
  33. package/docs/carbon.md +830 -0
  34. package/docs/changelog.md +465 -0
  35. package/docs/client/auth.md +113 -0
  36. package/docs/client/errors.md +139 -0
  37. package/docs/client/files.md +118 -0
  38. package/docs/client/index.md +128 -0
  39. package/docs/client/references.md +58 -0
  40. package/docs/client/requests.md +131 -0
  41. package/docs/client/resilience.md +141 -0
  42. package/docs/client/testing.md +146 -0
  43. package/docs/commands.md +450 -0
  44. package/docs/components.md +2427 -0
  45. package/docs/config-system.md +258 -0
  46. package/docs/container.md +596 -0
  47. package/docs/context.md +579 -0
  48. package/docs/contributing.md +164 -0
  49. package/docs/controllers.md +354 -0
  50. package/docs/conventions.md +466 -0
  51. package/docs/cookies.md +101 -0
  52. package/docs/csrf.md +206 -0
  53. package/docs/database.md +476 -0
  54. package/docs/deployment.md +434 -0
  55. package/docs/devtools.md +1013 -0
  56. package/docs/email-verification.md +320 -0
  57. package/docs/encryption.md +380 -0
  58. package/docs/errors.md +317 -0
  59. package/docs/events.md +420 -0
  60. package/docs/flow/components.md +846 -0
  61. package/docs/flow/decorators.md +458 -0
  62. package/docs/flow/events.md +420 -0
  63. package/docs/flow/forms.md +551 -0
  64. package/docs/flow/index.md +568 -0
  65. package/docs/flow/layouts.md +793 -0
  66. package/docs/flow/lifecycle.md +336 -0
  67. package/docs/flow/pagination.md +402 -0
  68. package/docs/flow/performance.md +262 -0
  69. package/docs/flow/references.md +317 -0
  70. package/docs/flow/routing.md +550 -0
  71. package/docs/flow/testing.md +447 -0
  72. package/docs/getting-started.md +286 -0
  73. package/docs/health.md +269 -0
  74. package/docs/helpers.md +423 -0
  75. package/docs/i18n.md +414 -0
  76. package/docs/index.md +65 -0
  77. package/docs/inertia/build.md +95 -0
  78. package/docs/inertia/devtools.md +197 -0
  79. package/docs/inertia/index.md +267 -0
  80. package/docs/inertia/middleware.md +81 -0
  81. package/docs/inertia/props.md +507 -0
  82. package/docs/inertia/references.md +144 -0
  83. package/docs/inertia/rendering.md +189 -0
  84. package/docs/inertia/ssr.md +102 -0
  85. package/docs/inspirations.md +257 -0
  86. package/docs/lifecycle.md +331 -0
  87. package/docs/lock.md +454 -0
  88. package/docs/logger.md +517 -0
  89. package/docs/media.md +582 -0
  90. package/docs/middleware.md +509 -0
  91. package/docs/migrations.md +529 -0
  92. package/docs/monitor.md +377 -0
  93. package/docs/notifications.md +848 -0
  94. package/docs/orm/casts.md +365 -0
  95. package/docs/orm/factories.md +231 -0
  96. package/docs/orm/index.md +663 -0
  97. package/docs/orm/lifecycle.md +421 -0
  98. package/docs/orm/queries.md +692 -0
  99. package/docs/orm/relationships.md +440 -0
  100. package/docs/orm/serialization.md +251 -0
  101. package/docs/package-development.md +536 -0
  102. package/docs/pagination.md +340 -0
  103. package/docs/password-reset.md +335 -0
  104. package/docs/providers.md +447 -0
  105. package/docs/query-builder.md +465 -0
  106. package/docs/queue.md +549 -0
  107. package/docs/rate-limiting.md +253 -0
  108. package/docs/responses.md +336 -0
  109. package/docs/roles-and-2fa.md +418 -0
  110. package/docs/routing.md +1008 -0
  111. package/docs/scaffolding.md +335 -0
  112. package/docs/scheduler.md +533 -0
  113. package/docs/seeding.md +229 -0
  114. package/docs/session.md +451 -0
  115. package/docs/social.md +524 -0
  116. package/docs/storage.md +622 -0
  117. package/docs/structure.md +230 -0
  118. package/docs/support-policy.md +114 -0
  119. package/docs/telemetry.md +505 -0
  120. package/docs/tenancy.md +608 -0
  121. package/docs/testing/browser.md +153 -0
  122. package/docs/testing/console.md +130 -0
  123. package/docs/testing/database.md +238 -0
  124. package/docs/testing/flow-browser.md +216 -0
  125. package/docs/testing/http.md +441 -0
  126. package/docs/testing/index.md +240 -0
  127. package/docs/testing/mocking.md +410 -0
  128. package/docs/upgrade.md +192 -0
  129. package/docs/validator.md +549 -0
  130. package/docs/view.md +453 -0
  131. package/package.json +60 -0
  132. package/src/bin/mcp.ts +73 -0
  133. package/src/config.ts +63 -0
  134. package/src/errors.ts +27 -0
  135. package/src/index.ts +56 -0
  136. package/src/install/ArchInstallCommand.ts +189 -0
  137. package/src/install/detect.ts +98 -0
  138. package/src/install/guidelines.ts +290 -0
  139. package/src/install/markers.ts +77 -0
  140. package/src/install/mcpConfig.ts +81 -0
  141. package/src/mcp/index.ts +27 -0
  142. package/src/mcp/jsonrpc.ts +133 -0
  143. package/src/mcp/server.ts +307 -0
  144. package/src/mcp/stdio.ts +111 -0
  145. package/src/mcp/types.ts +146 -0
  146. package/src/probe/ArchProbeCommand.ts +48 -0
  147. package/src/probe/sentinel.ts +13 -0
  148. package/src/probe/topics.ts +291 -0
  149. package/src/provider/ArchProvider.ts +101 -0
  150. package/src/tools/_probe.ts +164 -0
  151. package/src/tools/apiSurface.ts +241 -0
  152. package/src/tools/appInfo.ts +88 -0
  153. package/src/tools/baselines.ts +210 -0
  154. package/src/tools/context.ts +37 -0
  155. package/src/tools/doctor.ts +93 -0
  156. package/src/tools/index.ts +50 -0
  157. package/src/tools/logs.ts +265 -0
  158. package/src/tools/routes.ts +133 -0
  159. package/src/tools/schema.ts +119 -0
  160. package/src/tools/searchDocs.ts +345 -0
@@ -0,0 +1,101 @@
1
+ import { ServiceProvider } from "@zerotal/core";
2
+ import type { AppEnvironment, DoctorCheck } from "@zerotal/core";
3
+ import { SERVER_ENTRY_PATH } from "../install/mcpConfig.ts";
4
+
5
+ /**
6
+ * Registers the agent-surface commands.
7
+ *
8
+ * Binds nothing. The MCP server is a separate process that never boots an
9
+ * application — see `src/bin/mcp.ts` for why — so there is no manager to put in
10
+ * the container and no request-time behaviour to add. What this provider is for
11
+ * is the three commands, and the check that says whether the server has actually
12
+ * been wired up.
13
+ *
14
+ * `console` only. Every command here is a developer tool, and a web process has
15
+ * no reason to carry them.
16
+ *
17
+ * Register in `bootstrap/providers.ts`:
18
+ *
19
+ * ```typescript
20
+ * import { ArchProvider } from "@zerotal/arch";
21
+ *
22
+ * Application.create()
23
+ * .register([DatabaseProvider, ArchProvider])
24
+ * ```
25
+ */
26
+ export class ArchProvider extends ServiceProvider {
27
+ static override provides = [] as const;
28
+ static override environments: AppEnvironment[] = ["console"];
29
+
30
+ override async onBooted(): Promise<void> {
31
+ const runner = this.app.container.tryMake("commands");
32
+ if (!runner) return;
33
+
34
+ runner.registerLazy(
35
+ "arch:install",
36
+ () => import("../install/ArchInstallCommand.ts").then((m) => m.ArchInstallCommand),
37
+ ["arch:update"],
38
+ );
39
+ runner.registerLazy("arch:probe", () =>
40
+ import("../probe/ArchProbeCommand.ts").then((m) => m.ArchProbeCommand),
41
+ );
42
+ }
43
+
44
+ /**
45
+ * One check: is the MCP server registered where an agent will find it?
46
+ *
47
+ * A project that installed this package and never ran `arch:install` has an
48
+ * agent surface that exists and is not connected to anything — which looks
49
+ * exactly like it working until someone notices no tool was ever called.
50
+ */
51
+ override doctorChecks(): DoctorCheck[] {
52
+ return [
53
+ {
54
+ id: "arch-mcp-config",
55
+ label: "Agent surface",
56
+ run: async () => {
57
+ const entry = Bun.file(`${process.cwd()}/${SERVER_ENTRY_PATH}`);
58
+ if (!(await entry.exists())) {
59
+ return {
60
+ status: "warn" as const,
61
+ message:
62
+ "@zerotal/arch is registered but its server entry is missing from node_modules.",
63
+ fix: "bun install",
64
+ };
65
+ }
66
+
67
+ const config = Bun.file(`${process.cwd()}/.mcp.json`);
68
+ if (!(await config.exists())) {
69
+ return {
70
+ status: "warn" as const,
71
+ message:
72
+ "No .mcp.json, so no agent is connected to this app's MCP server — the tools " +
73
+ "exist but nothing can call them.",
74
+ fix: "bun zt arch:install",
75
+ };
76
+ }
77
+
78
+ try {
79
+ const document = (await config.json()) as Record<string, unknown>;
80
+ const servers = document["mcpServers"];
81
+ const names =
82
+ typeof servers === "object" && servers !== null ? Object.keys(servers) : [];
83
+ return names.length > 0
84
+ ? { status: "ok" as const, message: `.mcp.json registers ${names.join(", ")}` }
85
+ : {
86
+ status: "warn" as const,
87
+ message: ".mcp.json declares no MCP servers.",
88
+ fix: "bun zt arch:install",
89
+ };
90
+ } catch {
91
+ return {
92
+ status: "warn" as const,
93
+ message: ".mcp.json is not valid JSON, so no agent can read it.",
94
+ fix: "Fix the JSON, then run bun zt arch:install.",
95
+ };
96
+ }
97
+ },
98
+ },
99
+ ];
100
+ }
101
+ }
@@ -0,0 +1,164 @@
1
+ /**
2
+ * The one place a tool reaches into a booted application.
3
+ *
4
+ * Spawns `bun zt.ts arch:probe <topic>` in the project, reads the JSON the
5
+ * command frames with {@link PROBE_SENTINEL}, and hands it back. Everything the
6
+ * subprocess printed before the sentinel — banners, provider notices, a warning
7
+ * from a package — is discarded; only stderr is kept, and only to explain a
8
+ * failure.
9
+ *
10
+ * ## There is deliberately no cache
11
+ *
12
+ * Booting the app costs a second or so, and a burst of tool calls in one agent
13
+ * turn pays it each time. That is the trade being made on purpose: the caller is
14
+ * an agent that edits routes and models between calls, and an answer cached even
15
+ * for a few seconds can describe code that no longer exists. A slow correct
16
+ * answer is recoverable; a fast wrong one is what sends an agent down a path
17
+ * nothing will contradict.
18
+ */
19
+ import { dirname, join } from "node:path";
20
+ import { PROBE_SENTINEL } from "../probe/sentinel.ts";
21
+ import type { ProbeTopic } from "../probe/topics.ts";
22
+
23
+ /** How long a probe may run before it is killed. Boot plus a wide margin. */
24
+ const DEFAULT_TIMEOUT_MS = 60_000;
25
+
26
+ /** Entry filenames an app may use, in the order `bun zt` would find them. */
27
+ const ENTRY_NAMES = ["zt.ts", "zt.js"] as const;
28
+
29
+ export type ProbeResult = { ok: true; data: unknown } | { ok: false; message: string };
30
+
31
+ /** The seam tools depend on, so a tool's own tests never boot an application. */
32
+ export interface ProbeRunner {
33
+ run(topic: ProbeTopic, signal: AbortSignal): Promise<ProbeResult>;
34
+ }
35
+
36
+ export interface SpawnProbeOptions {
37
+ /** Where to start looking for the app. Defaults to the server's working directory. */
38
+ cwd?: string;
39
+ timeoutMs?: number;
40
+ }
41
+
42
+ /**
43
+ * Find the Zerotal app enclosing `start` — the nearest ancestor holding both a
44
+ * `package.json` and a `zt` entry point.
45
+ *
46
+ * Walks upward rather than trusting the working directory outright: an editor or
47
+ * an MCP client may launch the server from a subdirectory, and failing with
48
+ * "no app here" when the app is two levels up is a bad first impression.
49
+ */
50
+ export async function findApp(start: string): Promise<{ root: string; entry: string } | undefined> {
51
+ let dir = start;
52
+ for (;;) {
53
+ if (await Bun.file(join(dir, "package.json")).exists()) {
54
+ for (const name of ENTRY_NAMES) {
55
+ if (await Bun.file(join(dir, name)).exists()) return { root: dir, entry: name };
56
+ }
57
+ }
58
+ const parent = dirname(dir);
59
+ if (parent === dir) return undefined;
60
+ dir = parent;
61
+ }
62
+ }
63
+
64
+ /**
65
+ * Take the JSON a probe framed, ignoring whatever the app printed around it.
66
+ *
67
+ * The *last* sentinel wins: a log line quoting an earlier one cannot displace
68
+ * the real answer, because the real answer is always written last.
69
+ */
70
+ export function extractPayload(stdout: string): ProbeResult {
71
+ const marker = stdout.lastIndexOf(PROBE_SENTINEL);
72
+ if (marker === -1) {
73
+ return {
74
+ ok: false,
75
+ message: "The probe produced no report — the app printed nothing this tool could read.",
76
+ };
77
+ }
78
+
79
+ const body = stdout.slice(marker + PROBE_SENTINEL.length).trim();
80
+ if (body.length === 0) {
81
+ return { ok: false, message: "The probe framed an empty report." };
82
+ }
83
+
84
+ try {
85
+ return { ok: true, data: JSON.parse(body) as unknown };
86
+ } catch (error) {
87
+ return {
88
+ ok: false,
89
+ message: `The probe's report was not valid JSON: ${describe(error)}`,
90
+ };
91
+ }
92
+ }
93
+
94
+ /** The real runner: spawns the app's CLI and reads one topic out of it. */
95
+ export function spawnProbe(options: SpawnProbeOptions = {}): ProbeRunner {
96
+ const startDir = options.cwd ?? process.cwd();
97
+ const timeoutMs = options.timeoutMs ?? DEFAULT_TIMEOUT_MS;
98
+
99
+ return {
100
+ async run(topic, signal) {
101
+ const app = await findApp(startDir);
102
+ if (!app) {
103
+ return {
104
+ ok: false,
105
+ message:
106
+ `No Zerotal app found at or above ${startDir} — looked for a package.json beside ` +
107
+ `${ENTRY_NAMES.join(" or ")}. Run this server from the project root.`,
108
+ };
109
+ }
110
+
111
+ // `process.execPath` rather than "bun": the app must run on the same
112
+ // runtime as this server, and a PATH lookup can find a different one.
113
+ const child = Bun.spawn([process.execPath, app.entry, "arch:probe", topic], {
114
+ cwd: app.root,
115
+ stdout: "pipe",
116
+ stderr: "pipe",
117
+ // The probe reads config and the filesystem; it must not inherit a
118
+ // terminal it could block on.
119
+ stdin: "ignore",
120
+ });
121
+
122
+ let timedOut = false;
123
+ const timer = setTimeout(() => {
124
+ timedOut = true;
125
+ child.kill();
126
+ }, timeoutMs);
127
+ const abort = (): void => void child.kill();
128
+ signal.addEventListener("abort", abort, { once: true });
129
+
130
+ try {
131
+ const [stdout, stderr, code] = await Promise.all([
132
+ new Response(child.stdout).text(),
133
+ new Response(child.stderr).text(),
134
+ child.exited,
135
+ ]);
136
+
137
+ if (timedOut) {
138
+ return { ok: false, message: `The app did not boot within ${timeoutMs / 1000}s.` };
139
+ }
140
+ if (signal.aborted) return { ok: false, message: "Cancelled." };
141
+
142
+ const payload = extractPayload(stdout);
143
+ if (payload.ok) return payload;
144
+
145
+ // The app failed to boot, or the topic threw. Its stderr is the only
146
+ // thing that explains why, and it is exactly what the caller needs.
147
+ const detail = stderr.trim() || stdout.trim();
148
+ return {
149
+ ok: false,
150
+ message:
151
+ `\`bun ${app.entry} arch:probe ${topic}\` exited ${code}.` +
152
+ (detail ? `\n\n${detail}` : ` ${payload.message}`),
153
+ };
154
+ } finally {
155
+ clearTimeout(timer);
156
+ signal.removeEventListener("abort", abort);
157
+ }
158
+ },
159
+ };
160
+ }
161
+
162
+ function describe(error: unknown): string {
163
+ return error instanceof Error ? error.message : String(error);
164
+ }
@@ -0,0 +1,241 @@
1
+ /**
2
+ * `api_surface` — hand over the signature, not a description of it.
3
+ *
4
+ * Every `@zerotal/*` package commits an `api-surface.md`: the name, kind and
5
+ * full type of every public export, extracted by the TypeScript checker and
6
+ * diffed by CI on every change. It is the asset this whole package is built
7
+ * around. A docs search can tell an agent that a fluent builder exists; this
8
+ * tells it the exact call it must write for `tsc` to accept it.
9
+ *
10
+ * The file is read from the *installed* package — `node_modules/@zerotal/<pkg>`
11
+ * — so the answer describes the version this app actually runs. Inside the
12
+ * framework's own monorepo it falls back to `packages/<pkg>`, which is the same
13
+ * file by another path.
14
+ */
15
+ import { join } from "node:path";
16
+ import type { ArchTool, ToolOutcome } from "../mcp/types.ts";
17
+ import type { ToolContext } from "./context.ts";
18
+
19
+ /** One exported symbol, as recorded in the snapshot. */
20
+ export interface SurfaceEntry {
21
+ /** The subpath it is exported from, e.g. `.` or `./config`. */
22
+ subpath: string;
23
+ /** `class` | `interface` | `type` | `function` | `const` | `enum` | `namespace` | `value`. */
24
+ kind: string;
25
+ name: string;
26
+ /** The full rendered signature, class bodies included. */
27
+ signature: string;
28
+ }
29
+
30
+ /** A line that opens an entry: `class Foo = {`, `function bar = (…) => …`. */
31
+ const ENTRY_START = /^(class|interface|type|enum|function|namespace|const|value) (\S+) = ?(.*)$/;
32
+ /** A section heading: `## ./config \`(src/config/index.ts)\`` */
33
+ const SECTION_START = /^## (\S+)/;
34
+
35
+ /**
36
+ * Parse a committed `api-surface.md` into its entries.
37
+ *
38
+ * Line-driven rather than split-on-blank-line: a rendered class body is
39
+ * multi-line, and a signature that happened to contain a blank line would
40
+ * silently truncate the export it belongs to.
41
+ */
42
+ export function parseSurface(markdown: string): SurfaceEntry[] {
43
+ const entries: SurfaceEntry[] = [];
44
+ let subpath = ".";
45
+ let current: SurfaceEntry | undefined;
46
+ let body: string[] = [];
47
+
48
+ const flush = (): void => {
49
+ if (!current) return;
50
+ // Trailing blank lines are separators between entries, not part of one.
51
+ while (body.length > 0 && body[body.length - 1]!.trim() === "") body.pop();
52
+ entries.push({ ...current, signature: [current.signature, ...body].join("\n").trim() });
53
+ current = undefined;
54
+ body = [];
55
+ };
56
+
57
+ for (const line of markdown.split("\n")) {
58
+ const section = SECTION_START.exec(line);
59
+ if (section) {
60
+ flush();
61
+ subpath = section[1]!;
62
+ continue;
63
+ }
64
+
65
+ const start = ENTRY_START.exec(line);
66
+ if (start) {
67
+ flush();
68
+ current = { subpath, kind: start[1]!, name: start[2]!, signature: start[3] ?? "" };
69
+ continue;
70
+ }
71
+
72
+ if (current) body.push(line);
73
+ }
74
+ flush();
75
+
76
+ return entries;
77
+ }
78
+
79
+ /** Accept `core`, `@zerotal/core`, or the meta-package `zerotal`. */
80
+ export function normalisePackageName(raw: string): { scoped: string; dir: string } {
81
+ const trimmed = raw.trim().replace(/^@zerotal\//, "");
82
+ if (trimmed === "zerotal") return { scoped: "zerotal", dir: "zerotal" };
83
+ return { scoped: `@zerotal/${trimmed}`, dir: trimmed };
84
+ }
85
+
86
+ /**
87
+ * Where this project's copy of a package's snapshot lives.
88
+ *
89
+ * `node_modules` first: that is the version the app is running, and the only
90
+ * one whose signatures are the truth for the code being written. The workspace
91
+ * path is the fallback for the framework's own repo, where there is no install
92
+ * to read.
93
+ */
94
+ export async function findSurfaceFile(
95
+ root: string,
96
+ raw: string,
97
+ ): Promise<{ path: string; scoped: string } | undefined> {
98
+ const { scoped, dir } = normalisePackageName(raw);
99
+ const candidates = [
100
+ join(root, "node_modules", scoped, "api-surface.md"),
101
+ join(root, "packages", dir, "api-surface.md"),
102
+ ];
103
+ for (const path of candidates) {
104
+ if (await Bun.file(path).exists()) return { path, scoped };
105
+ }
106
+ return undefined;
107
+ }
108
+
109
+ /** Every package whose snapshot this project can serve, for the "did you mean" list. */
110
+ async function availablePackages(root: string): Promise<string[]> {
111
+ const names = new Set<string>();
112
+ for (const pattern of [
113
+ "node_modules/{zerotal,@zerotal/*}/api-surface.md",
114
+ "packages/*/api-surface.md",
115
+ ]) {
116
+ try {
117
+ for await (const file of new Bun.Glob(pattern).scan({ cwd: root, onlyFiles: true })) {
118
+ const parts = file.split(/[\\/]/);
119
+ const dir = parts[parts.length - 2] ?? "";
120
+ names.add(dir === "zerotal" ? "zerotal" : `@zerotal/${dir}`);
121
+ }
122
+ } catch {
123
+ /* a pattern that matches nothing contributes nothing */
124
+ }
125
+ }
126
+ return [...names].sort();
127
+ }
128
+
129
+ export function apiSurfaceTool(ctx: ToolContext): ArchTool {
130
+ return {
131
+ name: "api_surface",
132
+ title: "API surface",
133
+ description:
134
+ "The exact public API of a Zerotal package: every export with its full TypeScript " +
135
+ "signature, including class members and static properties. This is the mechanical " +
136
+ "record CI diffs on every change, read from the version installed in this project — " +
137
+ "prefer it over recalling an API from memory. Pass `symbol` to narrow to one export.",
138
+ inputSchema: {
139
+ type: "object",
140
+ properties: {
141
+ package: {
142
+ type: "string",
143
+ description: 'Package name, with or without the scope — "core" or "@zerotal/core".',
144
+ },
145
+ symbol: {
146
+ type: "string",
147
+ description:
148
+ "Only return exports whose name contains this, case-insensitively. Omit for all.",
149
+ },
150
+ },
151
+ required: ["package"],
152
+ additionalProperties: false,
153
+ },
154
+ outputSchema: {
155
+ type: "object",
156
+ properties: {
157
+ package: { type: "string" },
158
+ total: { type: "number" },
159
+ matched: { type: "number" },
160
+ entries: {
161
+ type: "array",
162
+ items: {
163
+ type: "object",
164
+ properties: {
165
+ subpath: { type: "string" },
166
+ kind: { type: "string" },
167
+ name: { type: "string" },
168
+ signature: { type: "string" },
169
+ },
170
+ required: ["subpath", "kind", "name", "signature"],
171
+ },
172
+ },
173
+ },
174
+ required: ["package", "total", "matched", "entries"],
175
+ },
176
+
177
+ async run(args): Promise<ToolOutcome> {
178
+ const requested = typeof args["package"] === "string" ? args["package"] : "";
179
+ if (requested.trim().length === 0) {
180
+ return { text: "`package` is required.", failed: true };
181
+ }
182
+
183
+ const found = await findSurfaceFile(ctx.root, requested);
184
+ if (!found) {
185
+ const available = await availablePackages(ctx.root);
186
+ return {
187
+ text:
188
+ `No API surface for "${requested}" in this project.` +
189
+ (available.length > 0 ? `\n\nAvailable: ${available.join(", ")}` : ""),
190
+ failed: true,
191
+ };
192
+ }
193
+
194
+ const all = parseSurface(await Bun.file(found.path).text());
195
+ const filter = typeof args["symbol"] === "string" ? args["symbol"].toLowerCase() : undefined;
196
+ const entries =
197
+ filter === undefined || filter.length === 0
198
+ ? all
199
+ : all.filter((entry) => entry.name.toLowerCase().includes(filter));
200
+
201
+ if (entries.length === 0) {
202
+ return {
203
+ text: `${found.scoped} exports nothing matching "${args["symbol"] as string}".`,
204
+ data: { package: found.scoped, total: all.length, matched: 0, entries: [] },
205
+ };
206
+ }
207
+
208
+ return {
209
+ text: render(found.scoped, entries, all.length),
210
+ data: {
211
+ package: found.scoped,
212
+ total: all.length,
213
+ matched: entries.length,
214
+ entries,
215
+ },
216
+ };
217
+ },
218
+ };
219
+ }
220
+
221
+ function render(pkg: string, entries: SurfaceEntry[], total: number): string {
222
+ const bySubpath = new Map<string, SurfaceEntry[]>();
223
+ for (const entry of entries) {
224
+ const list = bySubpath.get(entry.subpath) ?? [];
225
+ list.push(entry);
226
+ bySubpath.set(entry.subpath, list);
227
+ }
228
+
229
+ const header =
230
+ entries.length === total
231
+ ? `${pkg} — ${total} public export${total === 1 ? "" : "s"}`
232
+ : `${pkg} — ${entries.length} of ${total} exports`;
233
+
234
+ const sections = [...bySubpath].map(([subpath, list]) => {
235
+ const importPath = subpath === "." ? pkg : `${pkg}${subpath.slice(1)}`;
236
+ const body = list.map((entry) => `${entry.kind} ${entry.name} = ${entry.signature}`);
237
+ return `## import from "${importPath}"\n\n${body.join("\n\n")}`;
238
+ });
239
+
240
+ return `${header}\n\n${sections.join("\n\n")}`;
241
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * `app_info` — what this app is, before an agent assumes.
3
+ *
4
+ * The version and maturity of every installed `@zerotal/*` package matter more
5
+ * than they look. Zerotal ships source, so the installed version *is* the API,
6
+ * and `maturity` is the package's own statement about how much of it is
7
+ * promised. An agent that knows a package is `beta` writes different code from
8
+ * one that assumes everything is settled.
9
+ *
10
+ * The provider list is the other half: half the silent failures in a Zerotal app
11
+ * are a feature whose provider was never registered, and that is visible here
12
+ * before it is visible anywhere else.
13
+ */
14
+ import type { ArchTool, ToolOutcome } from "../mcp/types.ts";
15
+ import type { AppInfo } from "../probe/topics.ts";
16
+ import type { ToolContext } from "./context.ts";
17
+
18
+ export function appInfoTool(ctx: ToolContext): ArchTool {
19
+ return {
20
+ name: "app_info",
21
+ title: "App info",
22
+ description:
23
+ "What this Zerotal app is: the Bun version, the environment it boots as, its configured " +
24
+ "URL, every registered service provider, the WebSocket paths it serves, and the version " +
25
+ "and maturity of every installed @zerotal package. Call it once at the start of a task — " +
26
+ "the installed versions decide which APIs exist, and the provider list decides which " +
27
+ "features are actually wired up.",
28
+ inputSchema: { type: "object", additionalProperties: false },
29
+ outputSchema: {
30
+ type: "object",
31
+ properties: {
32
+ bun: { type: "string" },
33
+ environment: {
34
+ type: "string",
35
+ description: "The boot mode: web, console, worker, test or repl.",
36
+ },
37
+ appEnv: { type: "string", description: "The deployment environment from config." },
38
+ url: { type: "string" },
39
+ providers: { type: "array", items: { type: "string" } },
40
+ webSocketPaths: { type: "array", items: { type: "string" } },
41
+ packages: {
42
+ type: "array",
43
+ items: {
44
+ type: "object",
45
+ properties: {
46
+ name: { type: "string" },
47
+ version: { type: "string" },
48
+ maturity: { type: "string", enum: ["stable", "beta", "experimental"] },
49
+ },
50
+ required: ["name", "version"],
51
+ },
52
+ },
53
+ },
54
+ required: ["bun", "environment", "appEnv", "providers", "packages", "webSocketPaths"],
55
+ },
56
+
57
+ async run(_args, signal): Promise<ToolOutcome> {
58
+ const result = await ctx.probe.run("app-info", signal);
59
+ if (!result.ok) return { text: result.message, failed: true };
60
+
61
+ const info = result.data as AppInfo;
62
+ return { text: render(info), data: info };
63
+ },
64
+ };
65
+ }
66
+
67
+ function render(info: AppInfo): string {
68
+ const lines: string[] = [
69
+ `Bun ${info.bun} · boots as ${info.environment} · APP_ENV ${info.appEnv}`,
70
+ ];
71
+ if (info.url) lines.push(`URL: ${info.url}`);
72
+
73
+ lines.push("", `Providers (${info.providers.length}):`, ` ${info.providers.join(", ")}`);
74
+
75
+ if (info.webSocketPaths.length > 0) {
76
+ lines.push("", `WebSocket paths: ${info.webSocketPaths.join(", ")}`);
77
+ }
78
+
79
+ lines.push("", `Installed packages (${info.packages.length}):`);
80
+ for (const pkg of info.packages) {
81
+ // Maturity is only worth the reader's attention when it is not `stable` —
82
+ // everything else is the promise they already assume.
83
+ const caveat = pkg.maturity && pkg.maturity !== "stable" ? ` [${pkg.maturity}]` : "";
84
+ lines.push(` ${pkg.name}@${pkg.version}${caveat}`);
85
+ }
86
+
87
+ return lines.join("\n");
88
+ }