okengine 0.2.3 → 0.2.5

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 (42) hide show
  1. package/README.md +30 -4
  2. package/docs/spec/example.md +12 -3
  3. package/docs/spec/four-applications.md +10 -3
  4. package/package.json +9 -40
  5. package/src/cli/dev-app-runner.ts +45 -6
  6. package/src/cli/dev.ts +136 -17
  7. package/src/cli/docker-cli.test.ts +10 -8
  8. package/src/cli/docker.ts +18 -7
  9. package/src/cli/hero-meta.test.ts +85 -0
  10. package/src/cli/hero-meta.ts +252 -0
  11. package/src/cli/load-config.images.test.ts +51 -0
  12. package/src/cli/load-config.ts +84 -6
  13. package/src/config/define-config.test.ts +61 -0
  14. package/src/config/index.ts +89 -6
  15. package/src/config/resolve-driver.test.ts +30 -0
  16. package/src/console/server/flows.ts +3 -1
  17. package/src/console/server/operator-db.test.ts +140 -2
  18. package/src/console/server/operator-db.ts +147 -1
  19. package/src/console/server/plugin.ts +20 -0
  20. package/src/console/server/serve.ts +8 -1
  21. package/src/console/server/state.ts +12 -1
  22. package/src/console/ui/dist/assets/{index-Bnf_3Hei.js → index-Dy4jht9P.js} +1 -1
  23. package/src/console/ui/dist/index.html +1 -1
  24. package/src/console/ui/shell/App.tsx +10 -2
  25. package/src/docker/compose.ts +45 -14
  26. package/src/docker/derive.ts +29 -9
  27. package/src/docker/docker.test.ts +28 -4
  28. package/src/docker/dockerfile.integration.test.ts +2 -0
  29. package/src/docker/index.ts +10 -0
  30. package/src/docker/stack-id.test.ts +86 -0
  31. package/src/docker/stack-id.ts +108 -0
  32. package/src/docker/stack.integration.test.ts +16 -7
  33. package/src/docker/types.ts +20 -1
  34. package/src/kernel/app.ts +39 -8
  35. package/src/kernel/boot-bind/store.test.ts +60 -0
  36. package/src/kernel/boot-bind/store.ts +142 -12
  37. package/src/kernel/boot.ts +28 -4
  38. package/src/mcp/server.ts +99 -57
  39. package/src/runtime/dev-request-log.test.ts +33 -0
  40. package/src/runtime/dev-request-log.ts +130 -0
  41. package/src/term.test.ts +98 -5
  42. package/src/term.ts +287 -6
package/src/kernel/app.ts CHANGED
@@ -45,6 +45,11 @@ import {
45
45
  type FxOperator,
46
46
  type NamedRef,
47
47
  } from "./fx.ts";
48
+ import {
49
+ currentDevSurface,
50
+ logDevRequest,
51
+ shouldLogDevRequests,
52
+ } from "../runtime/dev-request-log.ts";
48
53
  import {
49
54
  mergeHooks,
50
55
  runPipeline,
@@ -126,6 +131,11 @@ export interface OkeOptions {
126
131
  readonly archiveInputFields?: readonly string[];
127
132
  /** Active environment for {@link OkeApp.boot} (defaults to `dev`). */
128
133
  readonly env?: BootOptions["env"];
134
+ /**
135
+ * Local-server mode (`oke dev -s` / `OKE_STACK=1`) — force the `stack`
136
+ * driver profile at boot.
137
+ */
138
+ readonly stack?: BootOptions["stack"];
129
139
  /** Optional `oke.config.ts` document consumed at boot. */
130
140
  readonly config?: BootOptions["config"];
131
141
  /** Pre-built element runtimes (skip construction at boot when present). */
@@ -483,6 +493,7 @@ export function oke(options: OkeOptions): OkeApp {
483
493
  bootEnv = overrides?.env ?? options.env ?? "dev";
484
494
  const merged: BootOptions = {
485
495
  env: bootEnv,
496
+ stack: overrides?.stack ?? options.stack,
486
497
  config: overrides?.config ?? options.config,
487
498
  elements: overrides?.elements ?? options.elements,
488
499
  secrets: overrides?.secrets ?? options.secrets,
@@ -892,13 +903,31 @@ export function oke(options: OkeOptions): OkeApp {
892
903
  },
893
904
  execute,
894
905
  async fetch(request) {
906
+ const started = performance.now();
907
+ let flowLabel: string | undefined;
895
908
  const url = new URL(request.url);
896
909
  const method = request.method.toUpperCase();
897
910
 
911
+ const respond = (response: Response): Response => {
912
+ if (shouldLogDevRequests()) {
913
+ logDevRequest({
914
+ surface: currentDevSurface(),
915
+ method,
916
+ path: url.pathname,
917
+ flow: flowLabel,
918
+ status: response.status,
919
+ ms: Math.round(performance.now() - started),
920
+ });
921
+ }
922
+ return response;
923
+ };
924
+
898
925
  if (method === "GET" && url.pathname === "/_oke/client.json") {
899
- return new Response(JSON.stringify(routes), {
900
- headers: { "content-type": "application/json" },
901
- });
926
+ return respond(
927
+ new Response(JSON.stringify(routes), {
928
+ headers: { "content-type": "application/json" },
929
+ }),
930
+ );
902
931
  }
903
932
 
904
933
  if (method === "POST" && url.pathname.startsWith("/_oke/")) {
@@ -910,8 +939,9 @@ export function oke(options: OkeOptions): OkeApp {
910
939
  const target =
911
940
  flowsByName.get(`${unit}.${flowName}`) ?? flowsByName.get(flowName);
912
941
  if (!target) {
913
- return new Response("Not Found", { status: 404 });
942
+ return respond(new Response("Not Found", { status: 404 }));
914
943
  }
944
+ flowLabel = target.name;
915
945
  let internalInput: unknown;
916
946
  try {
917
947
  internalInput = await request.json();
@@ -924,15 +954,16 @@ export function oke(options: OkeOptions): OkeApp {
924
954
  { kind: "internal" } satisfies InternalTrigger,
925
955
  { request },
926
956
  );
927
- return encodeExecuteResult(internalResult);
957
+ return respond(encodeExecuteResult(internalResult));
928
958
  }
929
959
  }
930
960
 
931
961
  const matched = router.match(method, url.pathname);
932
962
  if (!matched) {
933
- return new Response("Not Found", { status: 404 });
963
+ return respond(new Response("Not Found", { status: 404 }));
934
964
  }
935
965
  const { value: binding, params } = matched;
966
+ flowLabel = binding.flow.name;
936
967
 
937
968
  let route = compiled.get(binding);
938
969
  if (!route && binding.trigger.kind === "http") {
@@ -946,7 +977,7 @@ export function oke(options: OkeOptions): OkeApp {
946
977
  if (route) {
947
978
  const parsed = await route.parseValidate(request, params);
948
979
  if (!parsed.ok) {
949
- return encodeFailure(parsed.failure);
980
+ return respond(encodeFailure(parsed.failure));
950
981
  }
951
982
  input = parsed.input;
952
983
  validated = true;
@@ -962,7 +993,7 @@ export function oke(options: OkeOptions): OkeApp {
962
993
  { request, params, validated },
963
994
  );
964
995
 
965
- return encodeExecuteResult(result);
996
+ return respond(encodeExecuteResult(result));
966
997
  },
967
998
  async dispatchSignal(signal, payload) {
968
999
  const name = resolveName(signal);
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Store binder — stack profile (+ env overrides).
3
+ */
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+ import { resolveKvDriverId, resolveSqlDriverId } from "./store.ts";
7
+
8
+ describe("bindStore driver resolution", () => {
9
+ const prev = {
10
+ stack: process.env.OKE_STACK,
11
+ sql: process.env.OKE_SQL_DRIVER,
12
+ kv: process.env.OKE_KV_DRIVER,
13
+ };
14
+
15
+ afterEach(() => {
16
+ if (prev.stack === undefined) delete process.env.OKE_STACK;
17
+ else process.env.OKE_STACK = prev.stack;
18
+ if (prev.sql === undefined) delete process.env.OKE_SQL_DRIVER;
19
+ else process.env.OKE_SQL_DRIVER = prev.sql;
20
+ if (prev.kv === undefined) delete process.env.OKE_KV_DRIVER;
21
+ else process.env.OKE_KV_DRIVER = prev.kv;
22
+ });
23
+
24
+ test("dev env keeps sqlite / memory from config", () => {
25
+ const options = {
26
+ config: {
27
+ drivers: {
28
+ store: {
29
+ sql: { dev: "sqlite", stack: "postgres", prod: "postgres" },
30
+ kv: { dev: "memory", stack: "redis", prod: "redis" },
31
+ },
32
+ },
33
+ },
34
+ };
35
+ expect(resolveSqlDriverId(options, "dev", false)).toBe("sqlite");
36
+ expect(resolveKvDriverId(options, "dev", false)).toBe("memory");
37
+ });
38
+
39
+ test("stack env uses stack profile (falls back to prod)", () => {
40
+ const options = {
41
+ config: {
42
+ drivers: {
43
+ store: {
44
+ sql: { dev: "sqlite", stack: "postgres", prod: "postgres" },
45
+ kv: { dev: "memory", prod: "redis" },
46
+ },
47
+ },
48
+ },
49
+ };
50
+ expect(resolveSqlDriverId(options, "stack", true)).toBe("postgres");
51
+ expect(resolveKvDriverId(options, "stack", true)).toBe("redis");
52
+ });
53
+
54
+ test("stack mode honours OKE_*_DRIVER overrides", () => {
55
+ process.env.OKE_SQL_DRIVER = "postgres";
56
+ process.env.OKE_KV_DRIVER = "redis";
57
+ expect(resolveSqlDriverId({}, "stack", true)).toBe("postgres");
58
+ expect(resolveKvDriverId({}, "stack", true)).toBe("redis");
59
+ });
60
+ });
@@ -2,42 +2,70 @@
2
2
  * Lazy store binder — loaded only when Store is declared.
3
3
  */
4
4
 
5
+ import {
6
+ resolveDriverId,
7
+ type ConfigEnv,
8
+ } from "../../config/index.ts";
5
9
  import { memoryDrivers } from "../../drivers/memory.ts";
10
+ import { postgresDriver } from "../../drivers/postgres.ts";
11
+ import { redisDriver } from "../../drivers/redis.ts";
12
+ import { sqliteDriver } from "../../drivers/sqlite.ts";
13
+ import type { KvDriver, SqlDriver } from "../../drivers/types.ts";
6
14
  import {
7
15
  createStoreRuntime,
8
16
  type StoreRuntime,
9
17
  } from "../../elements/store.ts";
10
- import {
11
- resolveDriverId,
12
- type ConfigEnv,
13
- } from "../../config/index.ts";
18
+ import type { StoreDecl } from "../../elements/store/declare.ts";
14
19
  import type { BootOptions } from "../boot.ts"; // type-only — no cycle at runtime
15
20
 
16
21
  /**
17
22
  * Construct a Store runtime and register facet declarations.
18
23
  *
24
+ * In stack mode (`env === "stack"` / `OKE_STACK=1`), SQL/KV resolve from the
25
+ * `stack` driver map (falling back to `prod`) and URLs from `.env.stack`.
26
+ *
19
27
  * @param options - Boot options
20
28
  * @param env - Active environment
21
29
  * @param now - Clock
30
+ * @param stack - Prefer compose URLs when opening postgres/redis
22
31
  */
23
32
  export function bindStore(
24
33
  options: BootOptions,
25
34
  env: ConfigEnv,
26
35
  now: () => number,
36
+ stack = false,
27
37
  ): StoreRuntime {
28
- const sqlId =
29
- resolveDriverId(options.config?.drivers?.store?.sql, env) ?? "memory";
30
- const kvId =
31
- resolveDriverId(options.config?.drivers?.store?.kv, env) ?? "memory";
32
- void sqlId;
33
- void kvId;
38
+ const sqlId = resolveSqlDriverId(options, env, stack);
39
+ const kvId = resolveKvDriverId(options, env, stack);
40
+ const sqlUrl = sqlUrlFor(sqlId, stack);
41
+ const kvUrl = kvUrlFor(kvId, stack);
42
+
43
+ const sqlBindings: Record<
44
+ string,
45
+ { name: string; primary: { url: string } }
46
+ > = {};
47
+ const kvBindings: Record<string, { url?: string }> = {};
48
+
49
+ for (const decl of options.stores ?? []) {
50
+ if (isSqlDecl(decl)) {
51
+ sqlBindings[decl.name] = {
52
+ name: decl.name,
53
+ primary: { url: sqlUrl },
54
+ };
55
+ } else if (isKvDecl(decl)) {
56
+ kvBindings[decl.name] = kvUrl !== undefined ? { url: kvUrl } : {};
57
+ }
58
+ }
59
+
34
60
  const store = createStoreRuntime({
35
61
  drivers: {
36
- sql: memoryDrivers.sql,
37
- kv: memoryDrivers.kv,
62
+ sql: sqlDriverFor(sqlId),
63
+ kv: kvDriverFor(kvId),
38
64
  files: memoryDrivers.files,
39
65
  index: memoryDrivers.index,
40
66
  },
67
+ sql: sqlBindings,
68
+ kv: kvBindings,
41
69
  now,
42
70
  });
43
71
  for (const decl of options.stores ?? []) {
@@ -45,3 +73,105 @@ export function bindStore(
45
73
  }
46
74
  return store;
47
75
  }
76
+
77
+ /**
78
+ * @param options - Boot options
79
+ * @param env - Active env
80
+ * @param stack - Stack mode
81
+ */
82
+ export function resolveSqlDriverId(
83
+ options: BootOptions,
84
+ env: ConfigEnv,
85
+ stack: boolean,
86
+ ): string {
87
+ const fromEnv = process.env.OKE_SQL_DRIVER?.trim();
88
+ if (stack && fromEnv) return fromEnv;
89
+ const resolved = resolveDriverId(options.config?.drivers?.store?.sql, env);
90
+ if (resolved) return resolved;
91
+ return stack ? "postgres" : "memory";
92
+ }
93
+
94
+ /**
95
+ * @param options - Boot options
96
+ * @param env - Active env
97
+ * @param stack - Stack mode
98
+ */
99
+ export function resolveKvDriverId(
100
+ options: BootOptions,
101
+ env: ConfigEnv,
102
+ stack: boolean,
103
+ ): string {
104
+ const fromEnv = process.env.OKE_KV_DRIVER?.trim();
105
+ if (stack && fromEnv) return fromEnv;
106
+ const resolved = resolveDriverId(options.config?.drivers?.store?.kv, env);
107
+ if (resolved) return resolved;
108
+ return stack ? "redis" : "memory";
109
+ }
110
+
111
+ function sqlDriverFor(id: string): SqlDriver {
112
+ switch (id) {
113
+ case "postgres":
114
+ return postgresDriver;
115
+ case "sqlite":
116
+ return sqliteDriver;
117
+ case "memory":
118
+ return memoryDrivers.sql;
119
+ default:
120
+ throw new Error(`oke boot: unknown sql driver "${id}"`);
121
+ }
122
+ }
123
+
124
+ function kvDriverFor(id: string): KvDriver {
125
+ switch (id) {
126
+ case "redis":
127
+ return redisDriver;
128
+ case "memory":
129
+ return memoryDrivers.kv;
130
+ default:
131
+ throw new Error(`oke boot: unknown kv driver "${id}"`);
132
+ }
133
+ }
134
+
135
+ function sqlUrlFor(sqlId: string, stack: boolean): string {
136
+ if (sqlId === "postgres") {
137
+ const url =
138
+ process.env.DATABASE_URL ?? process.env.OKE_STORE_SQL_URL ?? undefined;
139
+ if (!url) {
140
+ throw new Error(
141
+ stack
142
+ ? "oke boot: postgres driver needs DATABASE_URL (did `oke dev -s` write .env.stack?)"
143
+ : "oke boot: postgres driver needs DATABASE_URL",
144
+ );
145
+ }
146
+ return url;
147
+ }
148
+ if (sqlId === "sqlite") {
149
+ return process.env.OKE_SQLITE_URL ?? ".oke/app.sqlite";
150
+ }
151
+ return ":memory:";
152
+ }
153
+
154
+ function kvUrlFor(kvId: string, stack: boolean): string | undefined {
155
+ if (kvId !== "redis") return undefined;
156
+ const url = process.env.REDIS_URL ?? process.env.OKE_STORE_KV_URL ?? undefined;
157
+ if (!url) {
158
+ throw new Error(
159
+ stack
160
+ ? "oke boot: redis driver needs REDIS_URL (did `oke dev -s` write .env.stack?)"
161
+ : "oke boot: redis driver needs REDIS_URL",
162
+ );
163
+ }
164
+ return url;
165
+ }
166
+
167
+ function isSqlDecl(
168
+ decl: StoreDecl,
169
+ ): decl is Extract<StoreDecl, { facet: "sql" }> {
170
+ return decl.facet === "sql";
171
+ }
172
+
173
+ function isKvDecl(
174
+ decl: StoreDecl,
175
+ ): decl is Extract<StoreDecl, { facet: "kv" }> {
176
+ return decl.facet === "kv";
177
+ }
@@ -76,8 +76,17 @@ export interface ElementRuntimes {
76
76
 
77
77
  /** Declarations + options consumed by {@link bootApplication}. */
78
78
  export interface BootOptions {
79
- /** Active environment (defaults to `dev`). */
79
+ /**
80
+ * Active environment (defaults to `dev`, or `stack` when
81
+ * {@link stack} / `OKE_STACK=1`).
82
+ */
80
83
  readonly env?: ConfigEnv;
84
+ /**
85
+ * Local-server mode (`oke dev -s` / `OKE_STACK=1`): force driver maps to the
86
+ * `stack` profile and prefer compose URLs (`.env.stack`).
87
+ * When unset, derived from `process.env.OKE_STACK`.
88
+ */
89
+ readonly stack?: boolean;
81
90
  /** Optional `oke.config.ts` document. */
82
91
  readonly config?: OkeConfig;
83
92
  /** Pre-built runtimes (skip construction when present). */
@@ -243,9 +252,24 @@ async function loadBind<T>(name: string): Promise<T> {
243
252
  * @param options - Declarations, config, pre-built runtimes
244
253
  */
245
254
  export async function bootApplication(
246
- options: BootOptions = {},
255
+ input: BootOptions = {},
247
256
  ): Promise<BootResult> {
248
- const env: ConfigEnv = options.env ?? "dev";
257
+ const stack =
258
+ input.stack === true ||
259
+ (input.stack !== false && process.env.OKE_STACK === "1");
260
+ // `-s` always selects the `stack` driver profile — not a mix of test/dev +
261
+ // prod store overrides (templates often pin `env: "test"` for harnesses).
262
+ const env: ConfigEnv = stack ? "stack" : (input.env ?? "dev");
263
+ let config = input.config;
264
+ if (config === undefined) {
265
+ try {
266
+ const { loadOkeConfig } = await import("../cli/load-config.ts");
267
+ config = (await loadOkeConfig(process.cwd())).config;
268
+ } catch {
269
+ config = undefined;
270
+ }
271
+ }
272
+ const options: BootOptions = { ...input, config, env, stack };
249
273
  const pre = options.elements ?? {};
250
274
  const now = options.now ?? (() => Date.now());
251
275
  const needs = resolveElementNeeds(options);
@@ -340,7 +364,7 @@ export async function bootApplication(
340
364
  let store = pre.store;
341
365
  if (needs.store) {
342
366
  if (!store) {
343
- store = storeBind!.bindStore(options, env, now);
367
+ store = storeBind!.bindStore(options, env, now, stack);
344
368
  } else {
345
369
  for (const decl of options.stores ?? []) {
346
370
  store.register?.(decl);
package/src/mcp/server.ts CHANGED
@@ -8,6 +8,10 @@
8
8
  * adapters receive structured operator ids.
9
9
  */
10
10
 
11
+ import {
12
+ logDevRequest,
13
+ shouldLogDevRequests,
14
+ } from "../runtime/dev-request-log.ts";
11
15
  import {
12
16
  checkRequestSecurity,
13
17
  forbiddenResponse,
@@ -67,28 +71,48 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
67
71
  const transportSessions = new Set<string>();
68
72
 
69
73
  const fetch = async (request: Request): Promise<Response> => {
74
+ const started = performance.now();
75
+ let flowLabel: string | undefined;
76
+ const url = new URL(request.url);
77
+ const method = request.method.toUpperCase();
78
+
79
+ const respond = (response: Response): Response => {
80
+ if (shouldLogDevRequests()) {
81
+ logDevRequest({
82
+ surface: "MCP",
83
+ method,
84
+ path: url.pathname,
85
+ flow: flowLabel,
86
+ status: response.status,
87
+ ms: Math.round(performance.now() - started),
88
+ });
89
+ }
90
+ return response;
91
+ };
92
+
70
93
  const security = checkRequestSecurity(request, allowed);
71
- if (!security.ok) return forbiddenResponse(security.reason);
94
+ if (!security.ok) return respond(forbiddenResponse(security.reason));
72
95
 
73
- const url = new URL(request.url);
74
- if (request.method === "GET" && url.pathname === "/health") {
75
- return Response.json({ ok: true, surface: "mcp" });
96
+ if (method === "GET" && url.pathname === "/health") {
97
+ return respond(Response.json({ ok: true, surface: "mcp" }));
76
98
  }
77
99
 
78
- if (request.method !== "POST" || url.pathname !== "/mcp") {
79
- return new Response("Not Found", { status: 404 });
100
+ if (method !== "POST" || url.pathname !== "/mcp") {
101
+ return respond(new Response("Not Found", { status: 404 }));
80
102
  }
81
103
 
82
104
  const bearer = extractBearer(request.headers.get("authorization"));
83
105
  if (bearer === null) {
84
- return jsonRpcHttp(
85
- rpcError(
86
- null,
87
- RpcErrorCode.unauthorized,
88
- "Bearer token required",
89
- asData({ reason: "missing-token" }, "error"),
106
+ return respond(
107
+ jsonRpcHttp(
108
+ rpcError(
109
+ null,
110
+ RpcErrorCode.unauthorized,
111
+ "Bearer token required",
112
+ asData({ reason: "missing-token" }, "error"),
113
+ ),
114
+ 401,
90
115
  ),
91
- 401,
92
116
  );
93
117
  }
94
118
 
@@ -103,14 +127,16 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
103
127
  } catch (err) {
104
128
  const message =
105
129
  err instanceof SessionError ? err.message : "authentication failed";
106
- return jsonRpcHttp(
107
- rpcError(
108
- null,
109
- RpcErrorCode.unauthorized,
110
- message,
111
- asData({ reason: "auth-failed", message }, "error"),
130
+ return respond(
131
+ jsonRpcHttp(
132
+ rpcError(
133
+ null,
134
+ RpcErrorCode.unauthorized,
135
+ message,
136
+ asData({ reason: "auth-failed", message }, "error"),
137
+ ),
138
+ 401,
112
139
  ),
113
- 401,
114
140
  );
115
141
  }
116
142
 
@@ -122,22 +148,27 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
122
148
  try {
123
149
  body = await request.json();
124
150
  } catch {
125
- return jsonRpcHttp(
126
- rpcError(null, RpcErrorCode.parse, "invalid JSON body"),
127
- 400,
151
+ return respond(
152
+ jsonRpcHttp(
153
+ rpcError(null, RpcErrorCode.parse, "invalid JSON body"),
154
+ 400,
155
+ ),
128
156
  );
129
157
  }
130
158
 
131
159
  const parsed = parseJsonRpcRequest(body);
132
160
  if (!parsed.ok) {
133
- return jsonRpcHttp(
134
- rpcError(null, RpcErrorCode.invalidRequest, parsed.message),
135
- 400,
161
+ return respond(
162
+ jsonRpcHttp(
163
+ rpcError(null, RpcErrorCode.invalidRequest, parsed.message),
164
+ 400,
165
+ ),
136
166
  );
137
167
  }
138
168
 
139
169
  const { request: rpc } = parsed;
140
170
  const id: JsonRpcId = rpc.id;
171
+ flowLabel = rpc.method;
141
172
 
142
173
  switch (rpc.method) {
143
174
  case "initialize": {
@@ -149,10 +180,10 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
149
180
  serverInfo: { name: "okengine-mcp", version },
150
181
  sessionId,
151
182
  };
152
- return jsonRpcHttp(rpcSuccess(id, result));
183
+ return respond(jsonRpcHttp(rpcSuccess(id, result)));
153
184
  }
154
185
  case "ping":
155
- return jsonRpcHttp(rpcSuccess(id, { ok: true }));
186
+ return respond(jsonRpcHttp(rpcSuccess(id, { ok: true })));
156
187
  case "tools/list": {
157
188
  const listed = tools.listTools().map((t) => ({
158
189
  name: t.name,
@@ -163,22 +194,27 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
163
194
  destructiveHint: t.mutability === "write",
164
195
  },
165
196
  }));
166
- return jsonRpcHttp(
167
- rpcSuccess(id, {
168
- tools: listed,
169
- // Catalogue itself is data.
170
- _oke: asData({ count: listed.length }, "catalog"),
171
- }),
197
+ return respond(
198
+ jsonRpcHttp(
199
+ rpcSuccess(id, {
200
+ tools: listed,
201
+ // Catalogue itself is data.
202
+ _oke: asData({ count: listed.length }, "catalog"),
203
+ }),
204
+ ),
172
205
  );
173
206
  }
174
207
  case "tools/call": {
175
208
  const call = parseToolsCallParams(rpc.params);
176
209
  if (!call.ok) {
177
- return jsonRpcHttp(
178
- rpcError(id, RpcErrorCode.invalidParams, call.message),
179
- 400,
210
+ return respond(
211
+ jsonRpcHttp(
212
+ rpcError(id, RpcErrorCode.invalidParams, call.message),
213
+ 400,
214
+ ),
180
215
  );
181
216
  }
217
+ flowLabel = call.name;
182
218
  const result = await tools.callTool(
183
219
  requester,
184
220
  call.name,
@@ -193,34 +229,40 @@ export function createMcpServer(options: CreateMcpServerOptions): McpServer {
193
229
  : result.code === "not-found"
194
230
  ? RpcErrorCode.methodNotFound
195
231
  : RpcErrorCode.invalidParams;
196
- return jsonRpcHttp(
197
- rpcError(id, code, result.message, result.data),
198
- result.code === "unauthorized" ? 401 : 403,
232
+ return respond(
233
+ jsonRpcHttp(
234
+ rpcError(id, code, result.message, result.data),
235
+ result.code === "unauthorized" ? 401 : 403,
236
+ ),
199
237
  );
200
238
  }
201
239
  // MCP content blocks: text carries JSON of the inert envelope.
202
240
  // Agents must treat it as data (envelope.kind === "data").
203
- return jsonRpcHttp(
204
- rpcSuccess(id, {
205
- content: [
206
- {
207
- type: "text",
208
- text: JSON.stringify(result.data),
209
- },
210
- ],
211
- structuredContent: result.data,
212
- isError: false,
213
- }),
241
+ return respond(
242
+ jsonRpcHttp(
243
+ rpcSuccess(id, {
244
+ content: [
245
+ {
246
+ type: "text",
247
+ text: JSON.stringify(result.data),
248
+ },
249
+ ],
250
+ structuredContent: result.data,
251
+ isError: false,
252
+ }),
253
+ ),
214
254
  );
215
255
  }
216
256
  default:
217
- return jsonRpcHttp(
218
- rpcError(
219
- id,
220
- RpcErrorCode.methodNotFound,
221
- `method not found: ${rpc.method}`,
257
+ return respond(
258
+ jsonRpcHttp(
259
+ rpcError(
260
+ id,
261
+ RpcErrorCode.methodNotFound,
262
+ `method not found: ${rpc.method}`,
263
+ ),
264
+ 404,
222
265
  ),
223
- 404,
224
266
  );
225
267
  }
226
268
  };
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Dev request log gating and silence rules.
3
+ */
4
+
5
+ import { afterEach, describe, expect, test } from "bun:test";
6
+ import {
7
+ isSilentDevRequest,
8
+ shouldLogDevRequests,
9
+ } from "./dev-request-log.ts";
10
+
11
+ describe("dev-request-log", () => {
12
+ const prev = process.env.OKE_DEV_REQUEST_LOG;
13
+
14
+ afterEach(() => {
15
+ if (prev === undefined) delete process.env.OKE_DEV_REQUEST_LOG;
16
+ else process.env.OKE_DEV_REQUEST_LOG = prev;
17
+ });
18
+
19
+ test("shouldLogDevRequests follows OKE_DEV_REQUEST_LOG", () => {
20
+ process.env.OKE_DEV_REQUEST_LOG = "1";
21
+ expect(shouldLogDevRequests()).toBe(true);
22
+ process.env.OKE_DEV_REQUEST_LOG = "0";
23
+ expect(shouldLogDevRequests()).toBe(false);
24
+ });
25
+
26
+ test("isSilentDevRequest skips live, health, assets, client.json", () => {
27
+ expect(isSilentDevRequest("GET", "/console/live")).toBe(true);
28
+ expect(isSilentDevRequest("GET", "/health")).toBe(true);
29
+ expect(isSilentDevRequest("GET", "/assets/index.js")).toBe(true);
30
+ expect(isSilentDevRequest("GET", "/_oke/client.json")).toBe(true);
31
+ expect(isSilentDevRequest("POST", "/console/flows")).toBe(false);
32
+ });
33
+ });