argsbarg 6.1.2 → 6.1.4

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 (230) hide show
  1. package/CHANGELOG.md +74 -1
  2. package/README.md +17 -19
  3. package/bin/argsbarg +10 -0
  4. package/docs/README.md +4 -3
  5. package/docs/ai-skills.md +4 -2
  6. package/docs/bundled-docs.md +50 -25
  7. package/docs/cli-program.md +52 -10
  8. package/docs/config-schema.md +10 -11
  9. package/docs/configure.md +2 -0
  10. package/docs/decisions.md +40 -0
  11. package/docs/developing.md +43 -5
  12. package/docs/http-server.md +171 -0
  13. package/docs/json-schema-subset.md +51 -0
  14. package/docs/mcp.md +4 -2
  15. package/docs/output-schema.md +55 -62
  16. package/examples/formats.ts +6 -6
  17. package/examples/full-example/Formula/full-example.rb +35 -0
  18. package/examples/full-example/README.md +20 -21
  19. package/examples/full-example/docs/README.md +1 -1
  20. package/examples/full-example/docs/cli-schema.json +1790 -98
  21. package/examples/full-example/docs/cli.md +1990 -0
  22. package/examples/full-example/docs/http.md +30 -31
  23. package/examples/full-example/docs/mcp.md +8 -22
  24. package/examples/full-example/docs/openapi.json +798 -44
  25. package/examples/full-example/docs/skill.md +10 -10
  26. package/examples/full-example/justfile +11 -1
  27. package/examples/full-example/src/commands/render-json/__generated__/RenderJsonInputSchema.json +15 -0
  28. package/examples/full-example/src/commands/render-json/__generated__/index.ts +5 -0
  29. package/examples/full-example/src/commands/render-json/command.test.ts +46 -0
  30. package/examples/full-example/src/commands/render-json/command.ts +30 -0
  31. package/examples/full-example/src/commands/render-json/types.ts +9 -0
  32. package/examples/full-example/src/commands/status/__generated__/StatusJsonOutputSchema.json +15 -0
  33. package/examples/full-example/src/commands/status/__generated__/index.ts +2 -2
  34. package/examples/full-example/src/commands/status/command.ts +5 -13
  35. package/examples/full-example/src/commands/status/types.ts +1 -14
  36. package/examples/full-example/src/commands/workspaces/__generated__/WorkspaceNameInputSchema.json +15 -0
  37. package/examples/full-example/src/commands/workspaces/__generated__/index.ts +5 -0
  38. package/examples/full-example/src/commands/workspaces/command.test.ts +58 -0
  39. package/examples/full-example/src/commands/workspaces/command.ts +94 -0
  40. package/examples/full-example/src/commands/workspaces/types.ts +6 -0
  41. package/examples/full-example/src/db/index.test.ts +86 -0
  42. package/examples/full-example/src/db/index.ts +101 -0
  43. package/examples/full-example/src/db/migrate.test.ts +35 -0
  44. package/examples/full-example/src/db/migrate.ts +69 -0
  45. package/examples/full-example/src/db/migrations/001_workspaces.sql +6 -0
  46. package/examples/full-example/src/db/tables/workspaces.ts +66 -0
  47. package/examples/full-example/src/program.ts +11 -36
  48. package/examples/full-example/src/types/argsbarg.d.ts +11 -0
  49. package/examples/full-example/src/types/md.d.ts +4 -0
  50. package/examples/full-example/tsconfig.json +5 -2
  51. package/examples/mcp-test.ts +1 -2
  52. package/examples/minimal.ts +1 -7
  53. package/examples/nested.ts +1 -2
  54. package/examples/option-required.ts +1 -1
  55. package/examples/servers.ts +4 -5
  56. package/index.d.ts +431 -136
  57. package/package.json +19 -2
  58. package/src/builtins/builtins.test.ts +7 -7
  59. package/src/builtins/completion-bash.ts +1 -1
  60. package/src/builtins/completion-fish.ts +1 -1
  61. package/src/builtins/completion-group.ts +4 -4
  62. package/src/builtins/completion-simulate-shared.ts +9 -0
  63. package/src/builtins/completion-zsh.ts +1 -1
  64. package/src/builtins/config.test.ts +3 -3
  65. package/src/builtins/config.ts +9 -9
  66. package/src/builtins/configure-copy.ts +2 -2
  67. package/src/builtins/configure.ts +4 -4
  68. package/src/builtins/dispatch.ts +19 -18
  69. package/src/builtins/export.ts +7 -5
  70. package/src/builtins/http.ts +68 -0
  71. package/src/builtins/mcp.ts +28 -4
  72. package/src/builtins/presentation.ts +6 -6
  73. package/src/builtins/registry.ts +6 -6
  74. package/src/builtins/scopes.ts +2 -2
  75. package/src/builtins/version.ts +1 -1
  76. package/src/cli-tool/full-example-capabilities.test.ts +10 -15
  77. package/src/cli-tool/main.ts +1 -1
  78. package/src/cli-tool/program.ts +3 -2
  79. package/src/cli-tool/prompt.ts +1 -1
  80. package/src/cli-tool/run-schemagen.ts +1 -3
  81. package/src/cli-tool/schemagen/cleanup.ts +6 -7
  82. package/src/cli-tool/schemagen/discover-schema-roots.ts +66 -120
  83. package/src/cli-tool/schemagen/index.ts +2 -2
  84. package/src/cli-tool/schemagen/names.ts +8 -13
  85. package/src/cli-tool/schemagen/run.ts +21 -28
  86. package/src/cli-tool/schemagen/schemagen.test.ts +136 -46
  87. package/src/config/bindings.test.ts +1 -1
  88. package/src/config/bindings.ts +1 -1
  89. package/src/config/bootstrap.test.ts +1 -1
  90. package/src/config/bootstrap.ts +36 -4
  91. package/src/config/context.test.ts +1 -1
  92. package/src/config/context.ts +1 -1
  93. package/src/config/entry.ts +1 -1
  94. package/src/config/file.test.ts +1 -1
  95. package/src/config/file.ts +3 -3
  96. package/src/config/manifest.ts +1 -1
  97. package/src/config/resolve.test.ts +1 -1
  98. package/src/config/resolve.ts +1 -1
  99. package/src/config/schema.ts +1 -1
  100. package/src/config/validate.ts +1 -1
  101. package/src/{install → configure/artifacts}/binary-placement.test.ts +1 -1
  102. package/src/{install → configure/artifacts}/binary-placement.ts +1 -1
  103. package/src/{install → configure/artifacts}/gh-release-update.ts +1 -1
  104. package/src/{install → configure/artifacts}/install-validate.test.ts +3 -3
  105. package/src/{install → configure/artifacts}/mcp-config.ts +1 -1
  106. package/src/{install → configure/artifacts}/mcp-opencode.test.ts +1 -1
  107. package/src/{install → configure/artifacts}/mcp-opencode.ts +1 -1
  108. package/src/{install → configure/artifacts}/paths.ts +5 -5
  109. package/src/configure/artifacts/plan.ts +24 -0
  110. package/src/{install → configure/artifacts}/status.test.ts +1 -1
  111. package/src/{install → configure/artifacts}/status.ts +2 -2
  112. package/src/{install → configure/artifacts}/target-base.ts +1 -1
  113. package/src/{install → configure/artifacts}/target-detect.ts +1 -1
  114. package/src/{install → configure/artifacts}/target-effective.ts +3 -9
  115. package/src/{install → configure/artifacts}/target-mcp-cli.ts +1 -1
  116. package/src/{install → configure/artifacts}/target-mcp-json.ts +1 -1
  117. package/src/{install → configure/artifacts}/target-plan-build.ts +2 -2
  118. package/src/{install → configure/artifacts}/target-registry.ts +2 -2
  119. package/src/{install → configure/artifacts}/target-scope.ts +3 -3
  120. package/src/{install → configure/artifacts}/target-skill.ts +1 -1
  121. package/src/{install → configure/artifacts}/target-types.ts +2 -2
  122. package/src/{install → configure/artifacts}/targets/app.ts +5 -5
  123. package/src/{install → configure/artifacts}/targets/chatgpt-mcp.ts +2 -2
  124. package/src/{install → configure/artifacts}/targets/claude-code-mcp.ts +2 -2
  125. package/src/{install → configure/artifacts}/targets/claude-desktop-mcp.ts +2 -2
  126. package/src/{install → configure/artifacts}/targets/claude-skill.ts +2 -2
  127. package/src/{install → configure/artifacts}/targets/codex-mcp.ts +2 -2
  128. package/src/{install → configure/artifacts}/targets/codex-skill.ts +2 -2
  129. package/src/{install → configure/artifacts}/targets/configure.ts +5 -5
  130. package/src/{install → configure/artifacts}/targets/cursor-mcp.ts +2 -2
  131. package/src/{install → configure/artifacts}/targets/cursor-skill.ts +2 -2
  132. package/src/{install → configure/artifacts}/targets/index.ts +1 -1
  133. package/src/{install → configure/artifacts}/targets/openclaw-mcp.ts +2 -2
  134. package/src/{install → configure/artifacts}/targets/openclaw-skill.ts +3 -3
  135. package/src/{install → configure/artifacts}/targets/opencode-mcp.ts +5 -5
  136. package/src/{install → configure/artifacts}/targets/opencode-skill.ts +3 -3
  137. package/src/{install → configure/artifacts}/targets.test.ts +1 -1
  138. package/src/{install → configure/artifacts}/uninstall.ts +1 -1
  139. package/src/configure/configure.test.ts +11 -11
  140. package/src/configure/index.ts +14 -14
  141. package/src/configure/prompt.ts +2 -2
  142. package/src/{context.ts → core/context.ts} +26 -20
  143. package/src/{json-leaf.test.ts → core/json-leaf.test.ts} +4 -4
  144. package/src/{leaf-inputs.test.ts → core/leaf-inputs.test.ts} +7 -7
  145. package/src/{leaf-inputs.ts → core/leaf-inputs.ts} +16 -12
  146. package/src/{parse.test.ts → core/parse.test.ts} +97 -109
  147. package/src/{parse.ts → core/parse.ts} +129 -31
  148. package/src/{schema.ts → core/schema.ts} +25 -13
  149. package/src/{types.ts → core/types.ts} +225 -35
  150. package/src/{validate.ts → core/validate.ts} +39 -29
  151. package/src/docs/builtin.ts +8 -19
  152. package/src/docs/{api-guide.test.ts → cli-guide.test.ts} +21 -21
  153. package/src/docs/{api-guide.ts → cli-guide.ts} +45 -16
  154. package/src/docs/docs.test.ts +76 -41
  155. package/src/docs/http-guide.ts +39 -36
  156. package/src/docs/mcp-guide.ts +12 -14
  157. package/src/docs/mcp-resources.test.ts +2 -3
  158. package/src/docs/mcp-resources.ts +6 -11
  159. package/src/docs/resolve.ts +22 -30
  160. package/src/docs/save.ts +3 -3
  161. package/src/exports/cli.ts +47 -0
  162. package/src/exports/headless.ts +13 -0
  163. package/src/exports/http.ts +6 -0
  164. package/src/exports/mcp.ts +6 -0
  165. package/src/{headless.test.ts → headless/routing.test.ts} +3 -3
  166. package/src/{headless.ts → headless/routing.ts} +3 -3
  167. package/src/headless/tool-call.ts +114 -46
  168. package/src/help.test.ts +152 -0
  169. package/src/help.ts +3 -3
  170. package/src/hooks/builtin.ts +20 -0
  171. package/src/hooks/run.ts +142 -0
  172. package/src/http/openapi.ts +290 -0
  173. package/src/http/readiness.ts +78 -0
  174. package/src/{api → http}/result.ts +22 -11
  175. package/src/http/routes.ts +329 -0
  176. package/src/http/server.ts +225 -0
  177. package/src/index.ts +36 -25
  178. package/src/log/ecs.test.ts +43 -0
  179. package/src/log/ecs.ts +59 -0
  180. package/src/log/emitter.ts +166 -0
  181. package/src/mcp/bundle.ts +2 -2
  182. package/src/mcp/claude.test.ts +1 -1
  183. package/src/mcp/claude.ts +4 -4
  184. package/src/{hidden-mcpb.test.ts → mcp/hidden-mcpb.test.ts} +10 -9
  185. package/src/mcp/result.ts +2 -2
  186. package/src/mcp/server.ts +54 -6
  187. package/src/mcp/tools.ts +9 -20
  188. package/src/{capabilities.ts → runtime/capabilities.ts} +11 -11
  189. package/src/{cli-errors.ts → runtime/cli-errors.ts} +4 -4
  190. package/src/{cli.ts → runtime/cli.ts} +159 -49
  191. package/src/runtime/exposure.ts +102 -0
  192. package/src/{invoke.test.ts → runtime/invoke.test.ts} +31 -7
  193. package/src/server/context.ts +25 -0
  194. package/src/server/overrides.ts +112 -0
  195. package/src/skill/generate.ts +8 -8
  196. package/src/skill/hint.ts +1 -1
  197. package/src/skill/install.ts +2 -2
  198. package/src/skill/naming.ts +1 -1
  199. package/src/{test-fixtures.ts → test/fixtures.ts} +3 -2
  200. package/src/{config.integration.test.ts → test/integration/config.test.ts} +8 -8
  201. package/src/test/integration/http.test.ts +651 -0
  202. package/src/{mcp.integration.test.ts → test/integration/mcp.test.ts} +11 -57
  203. package/docs/api-server.md +0 -141
  204. package/examples/full-example/docs/api.md +0 -511
  205. package/examples/full-example/src/commands/status/__generated__/outputSchema.json +0 -28
  206. package/examples/full-example/src/config/__generated__/configSchema.json +0 -40
  207. package/examples/full-example/src/config/__generated__/index.ts +0 -5
  208. package/examples/full-example/src/config/types.ts +0 -24
  209. package/src/api/openapi.ts +0 -117
  210. package/src/api/server.ts +0 -120
  211. package/src/api.integration.test.ts +0 -441
  212. package/src/builtins/api.ts +0 -38
  213. package/src/hidden.ts +0 -30
  214. package/src/install/plan.ts +0 -53
  215. /package/src/{install → configure/artifacts}/detect-installed.ts +0 -0
  216. /package/src/{install → configure/artifacts}/gh-release-update.test.ts +0 -0
  217. /package/src/{install → configure/artifacts}/mcp-codex.test.ts +0 -0
  218. /package/src/{install → configure/artifacts}/mcp-codex.ts +0 -0
  219. /package/src/{install → configure/artifacts}/mcp-openclaw.test.ts +0 -0
  220. /package/src/{install → configure/artifacts}/mcp-openclaw.ts +0 -0
  221. /package/src/{install → configure/artifacts}/normalize-uninstall.ts +0 -0
  222. /package/src/{install → configure/artifacts}/normalize.ts +0 -0
  223. /package/src/{install → configure/artifacts}/opts.ts +0 -0
  224. /package/src/{install → configure/artifacts}/shell.ts +0 -0
  225. /package/src/{formats.test.ts → core/formats.test.ts} +0 -0
  226. /package/src/{formats.ts → core/formats.ts} +0 -0
  227. /package/src/{respond.ts → core/respond.ts} +0 -0
  228. /package/src/{types.test.ts → core/types.test.ts} +0 -0
  229. /package/src/{api → http}/schema-deref.test.ts +0 -0
  230. /package/src/{api → http}/schema-deref.ts +0 -0
@@ -0,0 +1,225 @@
1
+ /*
2
+ HTTP tool server for ArgsBarg programs: health, OpenAPI, and REST API invocation.
3
+ */
4
+
5
+ import { randomUUID } from "node:crypto";
6
+ import type { CliHttpWireContext, CliProgram } from "~/core/types.ts";
7
+ import {
8
+ executeHttpRouteCall,
9
+ headlessFailureToHttpResponse,
10
+ headlessSuccessToHttpResponse,
11
+ } from "~/headless/tool-call.ts";
12
+ import type { Cli } from "~/runtime/cli.ts";
13
+ import { leafHttpResponseDefaults } from "~/runtime/exposure.ts";
14
+ import type { ResolvedHttpServeConfig } from "~/server/overrides.ts";
15
+ import { generateOpenApi } from "./openapi.ts";
16
+ import { evaluateReadiness } from "./readiness.ts";
17
+ import { API_CORS_HEADERS, apiDocsHtml, apiErrorResponse, apiOptionsResponse } from "./result.ts";
18
+ import { defaultSuccessStatus, matchHttpRoute } from "./routes.ts";
19
+
20
+ const DEFAULT_HOST = "127.0.0.1";
21
+ const DEFAULT_PORT = 3000;
22
+
23
+ /** Resolved listen address for the HTTP API server. */
24
+ export function resolveHttpListenAddress(program: CliProgram): { hostname: string; port: number } {
25
+ const config = program.httpServer;
26
+ return {
27
+ hostname: config?.host ?? DEFAULT_HOST,
28
+ port: config?.port ?? DEFAULT_PORT,
29
+ };
30
+ }
31
+
32
+ /** Resolves client IP, optionally honoring X-Forwarded-For. */
33
+ export function resolveClientIp(request: Request, trustProxy: boolean): string {
34
+ if (trustProxy) {
35
+ const xff = request.headers.get("x-forwarded-for");
36
+ if (xff) {
37
+ return xff.split(",")[0]?.trim() ?? "unknown";
38
+ }
39
+ }
40
+ return "unknown";
41
+ }
42
+
43
+ /** Writes a JSON HTTP response with CORS headers. */
44
+ function jsonResponse(status: number, body: unknown): Response {
45
+ return new Response(JSON.stringify(body), {
46
+ status,
47
+ headers: {
48
+ ...API_CORS_HEADERS,
49
+ "content-type": "application/json; charset=utf-8",
50
+ },
51
+ });
52
+ }
53
+
54
+ function parseQuery(url: URL): Record<string, string> {
55
+ const out: Record<string, string> = {};
56
+ for (const [k, v] of url.searchParams.entries()) {
57
+ out[k] = v;
58
+ }
59
+ return out;
60
+ }
61
+
62
+ /** Handles one HTTP request for the API server. */
63
+ export async function handleApiRequest(
64
+ cli: Cli,
65
+ request: Request,
66
+ resolved?: ResolvedHttpServeConfig,
67
+ ): Promise<Response> {
68
+ const httpConfig = resolved ?? cli.server?.http;
69
+ const trustProxy = httpConfig?.trustProxy ?? cli.program.httpServer?.trustProxy ?? false;
70
+ const requestId = randomUUID();
71
+ const url = new URL(request.url);
72
+ const clientIp = resolveClientIp(request, trustProxy);
73
+ const wireCtx: CliHttpWireContext = {
74
+ request,
75
+ requestId,
76
+ clientIp,
77
+ path: url.pathname,
78
+ method: request.method,
79
+ };
80
+ const hooks = cli.server?.httpHooks ?? cli.program.httpServer?.hooks;
81
+ const emitter = cli.server?.emitter;
82
+ const started = performance.now();
83
+
84
+ const finish = async (response: Response, failureKind?: string, error?: unknown): Promise<Response> => {
85
+ const durationMs = Math.round(performance.now() - started);
86
+ if (failureKind && error !== undefined) {
87
+ await hooks?.onError?.({
88
+ ...wireCtx,
89
+ failureKind: failureKind as import("~/core/types.ts").InvokeFailureKind,
90
+ error,
91
+ });
92
+ } else {
93
+ await hooks?.onResponse?.({ ...wireCtx, status: response.status, durationMs });
94
+ }
95
+ emitter?.emitAccess({
96
+ method: request.method,
97
+ path: url.pathname,
98
+ status: response.status,
99
+ durationMs,
100
+ requestId,
101
+ clientIp,
102
+ });
103
+ return response;
104
+ };
105
+
106
+ await hooks?.onRequest?.(wireCtx);
107
+
108
+ if (request.method === "OPTIONS") {
109
+ return finish(apiOptionsResponse());
110
+ }
111
+
112
+ const root = cli.program;
113
+ const path = url.pathname;
114
+
115
+ if (request.method === "GET" && (path === "/health" || path === "/health/live")) {
116
+ return finish(jsonResponse(200, { ok: true }));
117
+ }
118
+
119
+ if (request.method === "GET" && path === "/health/ready") {
120
+ const runtime = cli.server?.runtime;
121
+ if (!runtime) {
122
+ return finish(jsonResponse(200, { ok: true }));
123
+ }
124
+ const readiness = await evaluateReadiness(root, "http", runtime, cli.appConfig);
125
+ return finish(jsonResponse(readiness.ok ? 200 : 503, readiness));
126
+ }
127
+
128
+ if (request.method === "GET" && path === "/openapi.json") {
129
+ return finish(jsonResponse(200, generateOpenApi(root)));
130
+ }
131
+
132
+ if (request.method === "GET" && path === "/swagger") {
133
+ return finish(
134
+ new Response(apiDocsHtml(), {
135
+ status: 200,
136
+ headers: {
137
+ ...API_CORS_HEADERS,
138
+ "content-type": "text/html; charset=utf-8",
139
+ },
140
+ }),
141
+ );
142
+ }
143
+
144
+ if (path.startsWith("/api")) {
145
+ const match = matchHttpRoute(root, request.method, path);
146
+ if (!match.ok) {
147
+ return finish(apiErrorResponse(404, { error: "Not found" }));
148
+ }
149
+
150
+ let body: Record<string, unknown> = {};
151
+ if (request.method === "POST" || request.method === "PUT" || request.method === "PATCH") {
152
+ const rawBody = await request.text();
153
+ if (rawBody.trim().length > 0) {
154
+ try {
155
+ const parsed = JSON.parse(rawBody);
156
+ if (typeof parsed !== "object" || parsed === null || Array.isArray(parsed)) {
157
+ return finish(apiErrorResponse(400, { error: "Request body must be a JSON object" }));
158
+ }
159
+ body = parsed as Record<string, unknown>;
160
+ } catch {
161
+ return finish(apiErrorResponse(400, { error: "Invalid JSON body" }));
162
+ }
163
+ }
164
+ }
165
+
166
+ const query = parseQuery(url);
167
+ const result = await executeHttpRouteCall(cli, match.route, match.pathParams, query, body, {
168
+ request,
169
+ clientIp,
170
+ requestId,
171
+ });
172
+ if (result.ok) {
173
+ const leafHttp = leafHttpResponseDefaults(match.route.leaf);
174
+ const hasBody = result.response.body !== undefined;
175
+ const methodDefault =
176
+ match.route.leaf.http?.successStatus ??
177
+ defaultSuccessStatus(match.route.method, hasBody && match.route.method !== "DELETE");
178
+ return finish(headlessSuccessToHttpResponse(result, leafHttp, methodDefault));
179
+ }
180
+ const obscure = httpConfig?.obscureUnexpected ?? false;
181
+ return finish(headlessFailureToHttpResponse(result, obscure), result.invokeResult?.failureKind, result.message);
182
+ }
183
+
184
+ if (path.startsWith("/tools")) {
185
+ return finish(apiErrorResponse(404, { error: "Not found" }));
186
+ }
187
+
188
+ return finish(apiErrorResponse(404, { error: "Not found" }));
189
+ }
190
+
191
+ /** Runs the HTTP API server until the process is interrupted. */
192
+ export async function httpServeHttp(cli: Cli, resolved?: ResolvedHttpServeConfig): Promise<never> {
193
+ const listen = resolved ?? {
194
+ hostname: resolveHttpListenAddress(cli.program).hostname,
195
+ port: resolveHttpListenAddress(cli.program).port,
196
+ trustProxy: cli.program.httpServer?.trustProxy ?? false,
197
+ obscureUnexpected: cli.program.httpServer?.errors?.obscureUnexpected ?? false,
198
+ log: { format: "json" as const, access: true, errors: true, dev: false },
199
+ };
200
+ const server = Bun.serve({
201
+ hostname: listen.hostname,
202
+ port: listen.port,
203
+ fetch: (request) => handleApiRequest(cli, request, listen),
204
+ });
205
+ const url = `http://${server.hostname}:${server.port}`;
206
+ const emitter = cli.server?.emitter;
207
+ if (emitter && listen.log.format === "text") {
208
+ emitter.emitLifecycle(
209
+ `${cli.program.key} ${cli.program.version} — HTTP API listening on ${url}`,
210
+ "http.server.start",
211
+ );
212
+ } else {
213
+ emitter?.emit({
214
+ level: "info",
215
+ message: `HTTP API listening on ${url}`,
216
+ action: "http.server.start",
217
+ labels: { url },
218
+ });
219
+ if (!emitter) {
220
+ process.stderr.write(`HTTP API listening on ${url}\n`);
221
+ }
222
+ }
223
+ await new Promise<never>(() => {});
224
+ throw new Error("HTTP API server stopped unexpectedly");
225
+ }
package/src/index.ts CHANGED
@@ -7,40 +7,21 @@ It gives consumers one stable import path without forcing them to know the inter
7
7
  module layout.
8
8
  */
9
9
 
10
- export { generateOpenApi, openApiJson } from "./api/openapi.ts";
11
- export { Cli, type CliInvokeKind, type CliInvokeResult } from "./cli.ts";
12
- export { cliErrWithHelp } from "./cli-errors.ts";
13
10
  export { displayAppConfigPath, resolveAppConfigPath } from "./config/file.ts";
14
- export type { CliLeafInputs } from "./context.ts";
15
- export { CliContext } from "./context.ts";
11
+ export type { CliLeafInputs } from "./core/context.ts";
12
+ export { CliContext } from "./core/context.ts";
16
13
  export {
17
14
  parseCommaList,
18
15
  parseDate,
19
16
  parseDateTime,
20
17
  parseDurationMs,
21
- } from "./formats.ts";
22
- export type { HeadlessContext } from "./headless.ts";
23
- export {
24
- formatDryRunMessage,
25
- requireYesInNonTty,
26
- shouldRunHeadless,
27
- shouldRunHeadlessWithPositionals,
28
- shouldRunHeadlessWithYes,
29
- wantsExplicitJson,
30
- } from "./headless.ts";
18
+ } from "./core/formats.ts";
31
19
  export {
32
20
  LeafInputError,
33
- loadLeafInputs,
34
21
  preloadPipableJson,
35
22
  readJsonOptionValue,
36
- readLeafInputs,
37
- readLeafInputsAsync,
38
- } from "./leaf-inputs.ts";
39
- export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
40
- export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
23
+ } from "./core/leaf-inputs.ts";
41
24
  export type {
42
- CliApiResponseConfig,
43
- CliApiServerConfig,
44
25
  CliAppConfig,
45
26
  CliAppConfigEntry,
46
27
  CliAppConfigResolveContext,
@@ -49,27 +30,57 @@ export type {
49
30
  CliConfigureTargets,
50
31
  CliDocsConfig,
51
32
  CliDocsTopic,
33
+ ClientErrorOverride,
52
34
  CliHandler,
35
+ CliHttpServerConfig,
36
+ CliHttpWireContext,
37
+ CliHttpWireHooks,
53
38
  CliInvocation,
39
+ CliInvokeHookResult,
54
40
  CliLeafKind,
41
+ CliLocals,
42
+ CliLogConfig,
55
43
  CliMcpBundleConfig,
56
44
  CliMcpResource,
57
45
  CliMcpServerConfig,
58
46
  CliMcpToolConfig,
47
+ CliMcpWireContext,
48
+ CliMcpWireHooks,
59
49
  CliOption,
60
50
  CliPositional,
61
51
  CliProgram,
52
+ CliProgramHooks,
62
53
  CliRespondBody,
63
54
  CliRespondOptions,
55
+ ErrorHookContext,
64
56
  InstallAgentIntegration,
65
57
  InstallTargetSpec,
58
+ InvokeFailureKind,
59
+ InvokeHookContext,
60
+ ReadinessContext,
66
61
  ResolvedInstallTarget,
67
- } from "./types.ts";
62
+ ServerRuntime,
63
+ ServerState,
64
+ } from "./core/types.ts";
68
65
  export {
69
66
  CliFallbackMode,
70
67
  CliOptionKind,
71
68
  CliSchemaValidationError,
72
69
  CliValueFormat,
73
70
  isJsonLeaf,
74
- } from "./types.ts";
71
+ } from "./core/types.ts";
72
+ export type { HeadlessContext } from "./headless/routing.ts";
73
+ export {
74
+ formatDryRunMessage,
75
+ requireYesInNonTty,
76
+ shouldRunHeadless,
77
+ shouldRunHeadlessWithPositionals,
78
+ shouldRunHeadlessWithYes,
79
+ wantsExplicitJson,
80
+ } from "./headless/routing.ts";
81
+ export { generateOpenApi, openApiJson } from "./http/openapi.ts";
82
+ export type { McpBundlePaths, PackMcpBundleOpts } from "./mcp/bundle.ts";
83
+ export { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle } from "./mcp/bundle.ts";
84
+ export { Cli, type CliInvokeKind, type CliInvokeResult } from "./runtime/cli.ts";
85
+ export { cliErrWithHelp } from "./runtime/cli-errors.ts";
75
86
  export { isInteractiveTty } from "./utils.ts";
@@ -0,0 +1,43 @@
1
+ /*
2
+ Unit tests for ECS log line formatting.
3
+ */
4
+
5
+ import { describe, expect, test } from "bun:test";
6
+ import { formatEcsLine } from "./ecs.ts";
7
+
8
+ describe("formatEcsLine", () => {
9
+ test("includes service fields and event action", () => {
10
+ const line = formatEcsLine(
11
+ { name: "myapp", version: "7.0.0" },
12
+ {
13
+ level: "info",
14
+ message: "HTTP API listening",
15
+ action: "http.server.start",
16
+ },
17
+ );
18
+ const parsed = JSON.parse(line) as Record<string, unknown>;
19
+ expect(parsed["service.name"]).toBe("myapp");
20
+ expect(parsed["service.version"]).toBe("7.0.0");
21
+ expect(parsed["event.action"]).toBe("http.server.start");
22
+ expect(parsed.message).toBe("HTTP API listening");
23
+ expect(parsed["log.level"]).toBe("info");
24
+ expect(typeof parsed["@timestamp"]).toBe("string");
25
+ });
26
+
27
+ test("includes error stack fields", () => {
28
+ const err = new Error("boom");
29
+ const line = formatEcsLine(
30
+ { name: "app", version: "1.0.0" },
31
+ {
32
+ level: "error",
33
+ message: "invoke failed",
34
+ action: "invoke.error",
35
+ error: err,
36
+ },
37
+ );
38
+ const parsed = JSON.parse(line) as Record<string, unknown>;
39
+ expect(parsed["error.message"]).toBe("boom");
40
+ expect(parsed["error.type"]).toBe("Error");
41
+ expect(String(parsed["error.stack_trace"])).toContain("boom");
42
+ });
43
+ });
package/src/log/ecs.ts ADDED
@@ -0,0 +1,59 @@
1
+ /*
2
+ Elastic Common Schema (ECS) JSON log line formatting for framework observability.
3
+ */
4
+
5
+ /** Severity label for ECS `log.level`. */
6
+ export type EcsLogLevel = "debug" | "info" | "warn" | "error";
7
+
8
+ /** Fields merged into every ECS log line. */
9
+ export interface EcsServiceFields {
10
+ name: string;
11
+ version: string;
12
+ }
13
+
14
+ /** Input for one ECS log event. */
15
+ export interface EcsLogEvent {
16
+ level: EcsLogLevel;
17
+ message: string;
18
+ action?: string;
19
+ labels?: Record<string, string | number | boolean>;
20
+ error?: unknown;
21
+ fields?: Record<string, unknown>;
22
+ }
23
+
24
+ function errorFields(error: unknown): Record<string, unknown> {
25
+ if (error instanceof Error) {
26
+ return {
27
+ "error.message": error.message,
28
+ "error.type": error.name,
29
+ ...(error.stack ? { "error.stack_trace": error.stack } : {}),
30
+ };
31
+ }
32
+ return { "error.message": String(error) };
33
+ }
34
+
35
+ /** Formats one ECS-compatible JSON log line (newline omitted). */
36
+ export function formatEcsLine(service: EcsServiceFields, event: EcsLogEvent): string {
37
+ const line: Record<string, unknown> = {
38
+ "@timestamp": new Date().toISOString(),
39
+ "log.level": event.level,
40
+ message: event.message,
41
+ "service.name": service.name,
42
+ "service.version": service.version,
43
+ };
44
+ if (event.action) {
45
+ line["event.action"] = event.action;
46
+ }
47
+ if (event.labels) {
48
+ for (const [key, value] of Object.entries(event.labels)) {
49
+ line[`labels.${key}`] = value;
50
+ }
51
+ }
52
+ if (event.fields) {
53
+ Object.assign(line, event.fields);
54
+ }
55
+ if (event.error !== undefined) {
56
+ Object.assign(line, errorFields(event.error));
57
+ }
58
+ return JSON.stringify(line);
59
+ }
@@ -0,0 +1,166 @@
1
+ /*
2
+ Framework log emitter: ECS json or human text to stderr with optional file tee.
3
+ */
4
+
5
+ import { appendFileSync, mkdirSync } from "node:fs";
6
+ import { dirname } from "node:path";
7
+ import type { CliProgram } from "~/core/types.ts";
8
+ import { type EcsLogEvent, type EcsLogLevel, formatEcsLine } from "./ecs.ts";
9
+
10
+ /** Resolved logging options for a server or invoke session. */
11
+ export interface ResolvedLogConfig {
12
+ format: "json" | "text";
13
+ file?: string;
14
+ access: boolean;
15
+ errors: boolean;
16
+ dev: boolean;
17
+ }
18
+
19
+ /** Options for {@link LogEmitter}. */
20
+ export interface LogEmitterOpts {
21
+ program: CliProgram;
22
+ resolved: ResolvedLogConfig;
23
+ }
24
+
25
+ const OBSCURE_CLIENT_MESSAGE = "An unexpected error occurred.";
26
+
27
+ /** Fixed client message when `obscureUnexpected` hides internal failures. */
28
+ export function obscureUnexpectedClientMessage(): string {
29
+ return OBSCURE_CLIENT_MESSAGE;
30
+ }
31
+
32
+ /** Merges program defaults with CLI flag / serve overrides. */
33
+ export function resolveLogConfig(program: CliProgram, overrides: Partial<ResolvedLogConfig> = {}): ResolvedLogConfig {
34
+ const log = program.log;
35
+ return {
36
+ format: overrides.format ?? log?.format ?? "json",
37
+ file: overrides.file ?? log?.file,
38
+ access: overrides.access ?? log?.access ?? true,
39
+ errors: overrides.errors ?? log?.errors ?? true,
40
+ dev: overrides.dev ?? false,
41
+ };
42
+ }
43
+
44
+ /** Tee framework logs to stderr and an optional append-only file. */
45
+ export class LogEmitter {
46
+ private readonly service: { name: string; version: string };
47
+ private readonly resolved: ResolvedLogConfig;
48
+
49
+ constructor(opts: LogEmitterOpts) {
50
+ this.service = { name: opts.program.key, version: opts.program.version };
51
+ this.resolved = opts.resolved;
52
+ }
53
+
54
+ get config(): ResolvedLogConfig {
55
+ return this.resolved;
56
+ }
57
+
58
+ /** Emits one log event to stderr (and optional file). */
59
+ emit(event: EcsLogEvent): void {
60
+ const line = this.formatLine(event);
61
+ process.stderr.write(`${line}\n`);
62
+ this.appendFile(line);
63
+ }
64
+
65
+ /** Human startup line or ECS/json event for lifecycle milestones. */
66
+ emitLifecycle(message: string, action: string, labels?: Record<string, string | number | boolean>): void {
67
+ if (this.resolved.format === "text") {
68
+ process.stderr.write(`${message}\n`);
69
+ this.appendFile(message);
70
+ return;
71
+ }
72
+ this.emit({ level: "info", message, action, labels });
73
+ }
74
+
75
+ /** Access log for one HTTP request or MCP RPC. */
76
+ emitAccess(fields: {
77
+ method: string;
78
+ path: string;
79
+ status: number;
80
+ durationMs: number;
81
+ requestId?: string;
82
+ clientIp?: string;
83
+ }): void {
84
+ if (!this.resolved.access) {
85
+ return;
86
+ }
87
+ if (this.resolved.format === "text") {
88
+ const rid = fields.requestId ? ` ${fields.requestId}` : "";
89
+ const line = `${fields.method} ${fields.path} ${fields.status} ${fields.durationMs}ms${rid}`;
90
+ process.stderr.write(`${line}\n`);
91
+ this.appendFile(line);
92
+ return;
93
+ }
94
+ this.emit({
95
+ level: "info",
96
+ message: `${fields.method} ${fields.path}`,
97
+ action: "http.access",
98
+ labels: {
99
+ ...(fields.requestId ? { request_id: fields.requestId } : {}),
100
+ ...(fields.clientIp ? { client_ip: fields.clientIp } : {}),
101
+ http_method: fields.method,
102
+ http_path: fields.path,
103
+ http_status: fields.status,
104
+ duration_ms: fields.durationMs,
105
+ },
106
+ });
107
+ }
108
+
109
+ /** Error log after the hook pipeline (real stack always included). */
110
+ emitInvokeError(
111
+ failureKind: string,
112
+ error: unknown,
113
+ clientMessage: string,
114
+ labels?: Record<string, string | number | boolean>,
115
+ ): void {
116
+ if (!this.resolved.errors) {
117
+ return;
118
+ }
119
+ this.emit({
120
+ level: failureKind === "unexpected" ? "error" : "warn",
121
+ message: clientMessage,
122
+ action: "invoke.error",
123
+ labels: { failure_kind: failureKind, ...labels },
124
+ error,
125
+ });
126
+ if (this.resolved.dev && error instanceof Error && error.stack) {
127
+ process.stderr.write(`${error.stack}\n`);
128
+ this.appendFile(error.stack);
129
+ }
130
+ }
131
+
132
+ private formatLine(event: EcsLogEvent): string {
133
+ if (this.resolved.format === "text") {
134
+ return this.formatTextLine(event);
135
+ }
136
+ return formatEcsLine(this.service, event);
137
+ }
138
+
139
+ private formatTextLine(event: EcsLogEvent): string {
140
+ const level = event.level.toUpperCase();
141
+ const action = event.action ? ` [${event.action}]` : "";
142
+ let line = `${level}${action}: ${event.message}`;
143
+ if (event.error instanceof Error && event.error.stack) {
144
+ line = `${line}\n${event.error.stack}`;
145
+ }
146
+ return line;
147
+ }
148
+
149
+ private appendFile(line: string): void {
150
+ const file = this.resolved.file;
151
+ if (!file) {
152
+ return;
153
+ }
154
+ try {
155
+ mkdirSync(dirname(file), { recursive: true });
156
+ appendFileSync(file, `${line}\n`, "utf8");
157
+ } catch {
158
+ // Best-effort file tee; stderr already has the line.
159
+ }
160
+ }
161
+ }
162
+
163
+ /** Maps ECS level strings for quick call sites. */
164
+ export function ecsLevel(level: EcsLogLevel): EcsLogLevel {
165
+ return level;
166
+ }
package/src/mcp/bundle.ts CHANGED
@@ -6,8 +6,8 @@ Expects `dist/<program.key>` as the compiled binary input.
6
6
  import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { basename, join, resolve } from "node:path";
9
- import { buildProgramUserConfig } from "../config/manifest.ts";
10
- import type { CliMcpBundleConfig, CliProgram } from "../types.ts";
9
+ import { buildProgramUserConfig } from "~/config/manifest.ts";
10
+ import type { CliMcpBundleConfig, CliProgram } from "~/core/types.ts";
11
11
  import { packClaudePlugin } from "./claude.ts";
12
12
  import { collectMcpTools, mcpServerId } from "./tools.ts";
13
13
  import { zipStore } from "./zip.ts";
@@ -7,7 +7,7 @@ import { execSync } from "node:child_process";
7
7
  import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
8
8
  import { tmpdir } from "node:os";
9
9
  import { join } from "node:path";
10
- import type { CliProgram } from "../types.ts";
10
+ import type { CliProgram } from "~/core/types.ts";
11
11
  import {
12
12
  defaultClaudePluginPaths,
13
13
  generatePluginManifest,
package/src/mcp/claude.ts CHANGED
@@ -17,10 +17,10 @@ import {
17
17
  } from "node:fs";
18
18
  import { tmpdir } from "node:os";
19
19
  import { basename, join, relative, resolve } from "node:path";
20
- import { buildPluginMcpEnvMapping, buildProgramUserConfig } from "../config/manifest.ts";
21
- import { generatePluginSkillBundle } from "../skill/generate.ts";
22
- import { applyPluginSkillHint } from "../skill/hint.ts";
23
- import type { CliMcpBundleConfig, CliProgram } from "../types.ts";
20
+ import { buildPluginMcpEnvMapping, buildProgramUserConfig } from "~/config/manifest.ts";
21
+ import type { CliMcpBundleConfig, CliProgram } from "~/core/types.ts";
22
+ import { generatePluginSkillBundle } from "~/skill/generate.ts";
23
+ import { applyPluginSkillHint } from "~/skill/hint.ts";
24
24
  import { defaultMcpBundlePaths, type PackMcpBundleOpts } from "./bundle.ts";
25
25
  import { mcpServerId } from "./tools.ts";
26
26
  import { type ZipFileEntry, zipStore } from "./zip.ts";
@@ -6,13 +6,13 @@ import { describe, expect, test } from "bun:test";
6
6
  import { mkdirSync, mkdtempSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
7
7
  import { tmpdir } from "node:os";
8
8
  import { join } from "node:path";
9
- import { exportPresentationBuiltins } from "./builtins/export.ts";
10
- import { cliParseRoot, cliPresentationRoot } from "./builtins/presentation.ts";
11
- import { cliHelpRender } from "./help.ts";
12
- import { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle, runMcpBundle } from "./mcp/bundle.ts";
13
- import { collectMcpTools } from "./mcp/tools.ts";
14
- import { cliSchemaExport } from "./schema.ts";
15
- import { CliOptionKind, type CliProgram } from "./types.ts";
9
+ import { exportPresentationBuiltins } from "~/builtins/export.ts";
10
+ import { cliParseRoot, cliPresentationRoot } from "~/builtins/presentation.ts";
11
+ import { cliSchemaExport } from "~/core/schema.ts";
12
+ import { CliOptionKind, type CliProgram } from "~/core/types.ts";
13
+ import { cliHelpRender } from "~/help.ts";
14
+ import { defaultMcpBundlePaths, generateMcpManifest, packMcpBundle, runMcpBundle } from "./bundle.ts";
15
+ import { collectMcpTools } from "./tools.ts";
16
16
 
17
17
  const hiddenFixture: CliProgram = {
18
18
  key: "myapp",
@@ -27,7 +27,8 @@ const hiddenFixture: CliProgram = {
27
27
  },
28
28
  {
29
29
  key: "secret",
30
- hidden: true,
30
+ cli: { hidden: true },
31
+ mcpTool: { hidden: true },
31
32
  description: "Hidden command.",
32
33
  handler: () => {},
33
34
  },
@@ -42,7 +43,7 @@ const hiddenFixture: CliProgram = {
42
43
  },
43
44
  {
44
45
  name: "secret-flag",
45
- hidden: true,
46
+ cli: { hidden: true },
46
47
  description: "Hidden option.",
47
48
  kind: CliOptionKind.Presence,
48
49
  },
package/src/mcp/result.ts CHANGED
@@ -2,8 +2,8 @@
2
2
  This module builds MCP tools/call success results from handler respond payloads.
3
3
  */
4
4
 
5
- import { encodeRespondBodyBase64 } from "../respond.ts";
6
- import type { CliRespondOptions } from "../types.ts";
5
+ import { encodeRespondBodyBase64 } from "~/core/respond.ts";
6
+ import type { CliRespondOptions } from "~/core/types.ts";
7
7
 
8
8
  /** Text content block in an MCP tool result. */
9
9
  export interface McpTextContent {