@setluca/mcp 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -42
- package/dist/annotations.js +45 -0
- package/dist/completions.js +90 -0
- package/dist/config.js +55 -29
- package/dist/errors.js +59 -14
- package/dist/generated/openapi-inputs.js +4324 -1429
- package/dist/generated/openapi-outputs.js +21992 -8516
- package/dist/generated/route-catalog.js +1480 -0
- package/dist/http.js +95 -103
- package/dist/index.js +16 -10
- package/dist/oauth-resolver.js +58 -36
- package/dist/observability.js +11 -3
- package/dist/openapi-zod.js +129 -65
- package/dist/operations/catalog.js +50 -0
- package/dist/operations/groups/analytics.js +122 -0
- package/dist/operations/groups/bookings.js +189 -0
- package/dist/operations/groups/broadcasts.js +106 -0
- package/dist/operations/groups/cadences.js +14 -0
- package/dist/operations/groups/callEvents.js +131 -0
- package/dist/operations/groups/campaigns.js +132 -0
- package/dist/operations/groups/capabilities.js +8 -0
- package/dist/operations/groups/coach.js +43 -0
- package/dist/operations/groups/conversations.js +48 -0
- package/dist/operations/groups/insights.js +33 -0
- package/dist/operations/groups/integrations.js +80 -0
- package/dist/operations/groups/knowledge.js +34 -0
- package/dist/operations/groups/leads.js +101 -0
- package/dist/operations/groups/learning.js +24 -0
- package/dist/operations/groups/memory.js +27 -0
- package/dist/operations/groups/reports.js +14 -0
- package/dist/operations/groups/reviewQueue.js +57 -0
- package/dist/operations/groups/safety.js +8 -0
- package/dist/operations/groups/usage.js +13 -0
- package/dist/operations/groups/voice.js +14 -0
- package/dist/operations/groups/webhooks.js +76 -0
- package/dist/operations/manifest.js +40 -0
- package/dist/operations/registry.js +331 -0
- package/dist/operations.js +11 -780
- package/dist/optional-field.js +12 -0
- package/dist/page-shape.js +47 -0
- package/dist/pagination.js +164 -0
- package/dist/prompts.js +63 -8
- package/dist/provenance.js +18 -2
- package/dist/remote.js +80 -49
- package/dist/resilience.js +117 -0
- package/dist/serialization.js +36 -0
- package/dist/server.js +256 -281
- package/dist/task-tools/analytics.js +168 -0
- package/dist/task-tools/bookings.js +154 -0
- package/dist/task-tools/cadences.js +65 -0
- package/dist/task-tools/calls.js +202 -0
- package/dist/task-tools/inbox.js +74 -0
- package/dist/task-tools/leads.js +123 -0
- package/dist/task-tools/runtime.js +92 -0
- package/dist/task-tools.js +21 -0
- package/dist/tool-cache.js +39 -0
- package/dist/tool.js +228 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/version.js +14 -0
- package/package.json +36 -33
package/dist/http.js
CHANGED
|
@@ -1,103 +1,100 @@
|
|
|
1
|
-
import { Context, Effect, Layer } from "effect";
|
|
2
|
-
import
|
|
1
|
+
import { Context, Effect, Layer, Match, Option } from "effect";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, LucaConfig } from "./config.js";
|
|
3
4
|
import { LucaDecodeError, LucaHttpError, LucaNetworkError, } from "./errors.js";
|
|
5
|
+
import { optionalField } from "./optional-field.js";
|
|
6
|
+
import { resilient } from "./resilience.js";
|
|
7
|
+
import { fromJson, toJsonPayload } from "./serialization.js";
|
|
8
|
+
import { LUCA_MCP_USER_AGENT } from "./version.js";
|
|
4
9
|
export const LucaApi = Context.GenericTag("@luca/mcp/LucaApi");
|
|
5
10
|
function isPresent(value) {
|
|
6
11
|
return value !== undefined && value !== null && value !== "";
|
|
7
12
|
}
|
|
8
13
|
function encodePath(path, params) {
|
|
9
|
-
return path.
|
|
10
|
-
const value = params?.[key];
|
|
11
|
-
if (!value) {
|
|
12
|
-
throw new Error(`Missing path parameter: ${key}`);
|
|
13
|
-
}
|
|
14
|
-
return encodeURIComponent(value);
|
|
15
|
-
});
|
|
14
|
+
return path.replaceAll(/\{([^}]+)\}/g, (_, key) => Option.fromNullable(params?.[key]).pipe(Option.filter((value) => value.length > 0), Option.map(encodeURIComponent), Option.getOrThrowWith(() => new Error(`Missing path parameter: ${key}`))));
|
|
16
15
|
}
|
|
17
16
|
function appendQuery(url, query) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
if (isPresent(item)) {
|
|
25
|
-
url.searchParams.append(key, String(item));
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
else if (isPresent(value)) {
|
|
30
|
-
url.searchParams.set(key, String(value));
|
|
17
|
+
for (const [key, value] of Object.entries(query ?? {})) {
|
|
18
|
+
// A scalar is a one-element list here, so array and single-value params take
|
|
19
|
+
// the same path: append every value the caller actually set.
|
|
20
|
+
const values = Array.isArray(value) ? value : [value];
|
|
21
|
+
for (const item of values.filter(isPresent)) {
|
|
22
|
+
url.searchParams.append(key, String(item));
|
|
31
23
|
}
|
|
32
24
|
}
|
|
33
25
|
}
|
|
34
|
-
function parseJson(text) {
|
|
35
|
-
if (!text) {
|
|
36
|
-
return null;
|
|
37
|
-
}
|
|
38
|
-
try {
|
|
39
|
-
return JSON.parse(text);
|
|
40
|
-
}
|
|
41
|
-
catch {
|
|
42
|
-
throw new LucaDecodeError({
|
|
43
|
-
message: "Luca API returned non-JSON content",
|
|
44
|
-
body: text,
|
|
45
|
-
});
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
26
|
function buildUrl(config, input) {
|
|
49
27
|
const path = encodePath(input.operation.path, input.pathParams);
|
|
50
28
|
const url = new URL(path, `${config.apiBaseUrl}/`);
|
|
51
29
|
appendQuery(url, input.query);
|
|
52
30
|
return url;
|
|
53
31
|
}
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
});
|
|
59
|
-
setAuthHeaders(headers, config);
|
|
60
|
-
setWorkspaceHeaders(headers, config, input);
|
|
61
|
-
setPayloadHeaders(headers, input);
|
|
62
|
-
return headers;
|
|
32
|
+
/** The API key, under whichever header this config authenticates with. */
|
|
33
|
+
function authFields(config) {
|
|
34
|
+
return Match.value(config.authHeader).pipe(Match.when("authorization", () => ({
|
|
35
|
+
authorization: `Bearer ${config.apiKey}`,
|
|
36
|
+
})), Match.orElse(() => ({ "x-api-key": config.apiKey })));
|
|
63
37
|
}
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
38
|
+
const RETRY_AFTER_MS_PER_SECOND = 1000;
|
|
39
|
+
/**
|
|
40
|
+
* The wait a `Retry-After` header asks for, in milliseconds. The header comes
|
|
41
|
+
* in two forms — delta-seconds and an HTTP date — and both are read here so the
|
|
42
|
+
* retry rules never have to look at a header again. An unreadable value is the
|
|
43
|
+
* same as no header: the client falls back to its own backoff.
|
|
44
|
+
*/
|
|
45
|
+
function retryAfterMs(response, now) {
|
|
46
|
+
const header = response.headers.get("retry-after")?.trim();
|
|
47
|
+
if (!header) {
|
|
48
|
+
return;
|
|
67
49
|
}
|
|
68
|
-
|
|
69
|
-
|
|
50
|
+
const seconds = z.coerce.number().int().min(0).safeParse(header);
|
|
51
|
+
if (seconds.success) {
|
|
52
|
+
return seconds.data * RETRY_AFTER_MS_PER_SECOND;
|
|
70
53
|
}
|
|
54
|
+
const at = Date.parse(header);
|
|
55
|
+
return Number.isNaN(at) ? undefined : Math.max(0, at - now);
|
|
71
56
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
if (workspaceId) {
|
|
76
|
-
headers.set("x-luca-workspace-id", workspaceId);
|
|
77
|
-
}
|
|
78
|
-
if (workspaceSlug) {
|
|
79
|
-
headers.set("x-luca-workspace-slug", workspaceSlug);
|
|
80
|
-
}
|
|
57
|
+
/** The JSON payload of a request; absent for a GET or a bodiless call. */
|
|
58
|
+
function requestBody(input) {
|
|
59
|
+
return Option.liftPredicate(input, (candidate) => candidate.operation.method !== "GET" && candidate.body !== undefined).pipe(Option.map((candidate) => toJsonPayload(candidate.body)));
|
|
81
60
|
}
|
|
82
|
-
function
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
61
|
+
function buildRequestInit(config, input, requestIdempotencyKey) {
|
|
62
|
+
// The body decides its own content-type header, so both are read from the one
|
|
63
|
+
// value rather than from two independent recomputations of "does this have a
|
|
64
|
+
// body" that could drift apart.
|
|
65
|
+
const body = requestBody(input);
|
|
66
|
+
const headers = new Headers({
|
|
67
|
+
accept: "application/json",
|
|
68
|
+
"user-agent": LUCA_MCP_USER_AGENT,
|
|
69
|
+
...authFields(config),
|
|
70
|
+
...optionalField("x-luca-workspace-id", input.workspace?.workspaceId ?? config.workspaceId),
|
|
71
|
+
...optionalField("x-luca-workspace-slug", input.workspace?.workspaceSlug ?? config.workspaceSlug),
|
|
72
|
+
...optionalField("content-type", Option.getOrUndefined(Option.map(body, () => "application/json"))),
|
|
73
|
+
...optionalField("idempotency-key", requestIdempotencyKey),
|
|
74
|
+
});
|
|
93
75
|
return {
|
|
94
76
|
method: input.operation.method,
|
|
95
77
|
headers,
|
|
96
|
-
...(
|
|
78
|
+
...optionalField("body", Option.getOrUndefined(body)),
|
|
97
79
|
};
|
|
98
80
|
}
|
|
99
|
-
|
|
81
|
+
/** One trip to the API, with no retry and no timeout of its own. */
|
|
82
|
+
function sendOnce(config, input, requestIdempotencyKey) {
|
|
100
83
|
return Effect.gen(function* () {
|
|
84
|
+
const url = buildUrl(config, input);
|
|
85
|
+
const init = buildRequestInit(config, input, requestIdempotencyKey);
|
|
86
|
+
const response = yield* Effect.tryPromise({
|
|
87
|
+
try: (signal) =>
|
|
88
|
+
// fallow-ignore-next-line security-sink -- the origin is the configured Luca api base url and the path comes from the generated operation manifest with its params encoded, so a tool argument can never move the destination host
|
|
89
|
+
fetch(url, {
|
|
90
|
+
...init,
|
|
91
|
+
signal,
|
|
92
|
+
}),
|
|
93
|
+
catch: (cause) => new LucaNetworkError({
|
|
94
|
+
message: `Could not reach Luca API at ${url.toString()}`,
|
|
95
|
+
cause,
|
|
96
|
+
}),
|
|
97
|
+
});
|
|
101
98
|
const text = yield* Effect.tryPromise({
|
|
102
99
|
try: () => response.text(),
|
|
103
100
|
catch: (cause) => new LucaNetworkError({
|
|
@@ -105,41 +102,36 @@ function readResponseBody(response) {
|
|
|
105
102
|
cause,
|
|
106
103
|
}),
|
|
107
104
|
});
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
105
|
+
// An empty body is not a decode failure — several endpoints answer with
|
|
106
|
+
// no content at all, and null is the JSON value that stands for it.
|
|
107
|
+
const json = yield* Option.liftPredicate(text, (raw) => raw.length > 0).pipe(Option.match({
|
|
108
|
+
onNone: () => Effect.succeed(null),
|
|
109
|
+
onSome: (raw) => Effect.try({
|
|
110
|
+
try: () => fromJson(raw),
|
|
111
|
+
catch: () => new LucaDecodeError({
|
|
112
|
+
message: "Luca API returned non-JSON content",
|
|
113
|
+
body: raw,
|
|
114
|
+
}),
|
|
115
|
+
}),
|
|
116
|
+
}));
|
|
117
|
+
if (response.ok) {
|
|
118
|
+
return json;
|
|
119
|
+
}
|
|
120
|
+
return yield* Effect.fail(new LucaHttpError({
|
|
121
|
+
status: response.status,
|
|
122
|
+
statusText: response.statusText,
|
|
123
|
+
body: json,
|
|
124
|
+
...optionalField("requestId", response.headers.get("x-request-id")),
|
|
125
|
+
...optionalField("retryAfterMs", retryAfterMs(response, Date.now())),
|
|
126
|
+
}));
|
|
114
127
|
});
|
|
115
128
|
}
|
|
116
129
|
export function makeLucaApi(config) {
|
|
130
|
+
const timeoutMs = config.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
117
131
|
return {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
const response = yield* Effect.tryPromise({
|
|
122
|
-
try: (signal) => fetch(url, {
|
|
123
|
-
...init,
|
|
124
|
-
signal,
|
|
125
|
-
}),
|
|
126
|
-
catch: (cause) => new LucaNetworkError({
|
|
127
|
-
message: `Could not reach Luca API at ${url.toString()}`,
|
|
128
|
-
cause,
|
|
129
|
-
}),
|
|
130
|
-
});
|
|
131
|
-
const json = yield* readResponseBody(response);
|
|
132
|
-
if (!response.ok) {
|
|
133
|
-
const requestId = response.headers.get("x-request-id") ?? undefined;
|
|
134
|
-
return yield* Effect.fail(new LucaHttpError({
|
|
135
|
-
status: response.status,
|
|
136
|
-
statusText: response.statusText,
|
|
137
|
-
body: json,
|
|
138
|
-
...(requestId ? { requestId } : {}),
|
|
139
|
-
}));
|
|
140
|
-
}
|
|
141
|
-
return json;
|
|
142
|
-
}),
|
|
132
|
+
// The key each attempt carries is minted by `resilient`, which also decides
|
|
133
|
+
// whether a failure may be replayed under it.
|
|
134
|
+
request: (input) => resilient(input, timeoutMs, (key) => sendOnce(config, input, key)),
|
|
143
135
|
};
|
|
144
136
|
}
|
|
145
137
|
export const LucaApiLive = Layer.effect(LucaApi, Effect.gen(function* () {
|
package/dist/index.js
CHANGED
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
3
|
-
import { createLucaServer } from "./server.js";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
2
|
+
import { serveStdio } from "@modelcontextprotocol/server/stdio";
|
|
3
|
+
import { createLucaServer, parseToolset } from "./server.js";
|
|
4
|
+
/**
|
|
5
|
+
* `serveStdio` owns the era decision for the connection: the opening exchange
|
|
6
|
+
* picks either the 2026-07-28 protocol or the 2025 `initialize` handshake, and
|
|
7
|
+
* one instance from this factory is pinned for the connection's lifetime. The
|
|
8
|
+
* factory registers the same surface either way, so a client on either
|
|
9
|
+
* revision sees an identical toolset.
|
|
10
|
+
*/
|
|
11
|
+
serveStdio(() => createLucaServer({ toolset: parseToolset(process.env.LUCA_TOOLSET) }), {
|
|
12
|
+
// stdout carries the protocol, so every diagnostic goes to stderr. These
|
|
13
|
+
// are out-of-band errors: reporting them never changes what the client
|
|
14
|
+
// receives, and the connection stays up.
|
|
15
|
+
onerror: (error) => {
|
|
16
|
+
console.error("Luca MCP server error:", error);
|
|
17
|
+
},
|
|
12
18
|
});
|
package/dist/oauth-resolver.js
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { Duration, Effect, Option } from "effect";
|
|
2
|
+
import * as z from "zod/v4";
|
|
3
|
+
import { DEFAULT_REQUEST_TIMEOUT_MS, parseTimeoutMs, } from "./config.js";
|
|
1
4
|
const TRAILING_SLASHES = /\/+$/;
|
|
2
5
|
/**
|
|
3
6
|
* Developer API keys are handled by apiKeyResolver and never leave this
|
|
@@ -5,9 +8,47 @@ const TRAILING_SLASHES = /\/+$/;
|
|
|
5
8
|
* developer keys — they fall through to the resolver, which rejects them
|
|
6
9
|
* server-side (fail closed, no client-side oracle).
|
|
7
10
|
*/
|
|
8
|
-
const
|
|
11
|
+
const API_KEY_FIELDS = /^luca_(?!ort_)[a-f0-9]/i;
|
|
12
|
+
const ResolvePayloadSchema = z.object({
|
|
13
|
+
apiKey: z.string().optional(),
|
|
14
|
+
scopes: z.array(z.string()).optional(),
|
|
15
|
+
});
|
|
9
16
|
/**
|
|
10
|
-
*
|
|
17
|
+
* The body of a resolve response, absent when the API refused the token. A
|
|
18
|
+
* refusal is not read at all: its body carries nothing this resolver acts on.
|
|
19
|
+
*/
|
|
20
|
+
function resolvePayload(response) {
|
|
21
|
+
return Option.match(Option.liftPredicate(response, (res) => res.ok), {
|
|
22
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
23
|
+
onSome: (res) => Effect.tryPromise(() => res.json()).pipe(Effect.map((payload) => ResolvePayloadSchema.parse(payload)), Effect.map(Option.some)),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
/** The identity a payload names, absent when it names no vended key. */
|
|
27
|
+
function identityFromPayload(apiBaseUrl, authHeader, payload) {
|
|
28
|
+
return Option.map(Option.fromNullable(payload.apiKey), (apiKey) => ({
|
|
29
|
+
config: { apiBaseUrl, apiKey, authHeader },
|
|
30
|
+
scopes: payload.scopes ?? [],
|
|
31
|
+
}));
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* One authenticated call to POST /oauth/resolve, answered with the identity the
|
|
35
|
+
* token maps to. A network failure, a refusal, and an unreadable body are all
|
|
36
|
+
* the same answer — no identity — because the transport replies 401 to each of
|
|
37
|
+
* them and telling them apart would hand a caller an oracle.
|
|
38
|
+
*/
|
|
39
|
+
function requestIdentity(apiBaseUrl, authHeader, fetchImpl, token, timeoutMs) {
|
|
40
|
+
return Effect.tryPromise(() => fetchImpl(`${apiBaseUrl}/oauth/resolve`, {
|
|
41
|
+
method: "POST",
|
|
42
|
+
headers: { authorization: `Bearer ${token}` },
|
|
43
|
+
})).pipe(Effect.flatMap(resolvePayload), Effect.map((payload) => Option.flatMap(payload, (body) => identityFromPayload(apiBaseUrl, authHeader, body))),
|
|
44
|
+
// Timeout but no retry. This call sits on the auth path of every request,
|
|
45
|
+
// has no idempotency to reason about, and already answers "no identity" for
|
|
46
|
+
// every failure — a plain `timeout` is enough because `orElseSucceed`
|
|
47
|
+
// discards the error shape anyway.
|
|
48
|
+
Effect.timeout(Duration.millis(timeoutMs)), Effect.orElseSucceed(() => Option.none()));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Resolver for OAuth access tokens (docs/runbooks/mcp-oauth-authorization-server.md
|
|
11
52
|
* §5.2): one authenticated call to the Luca API's POST /oauth/resolve, which
|
|
12
53
|
* verifies the JWT and returns the hidden vended key for the token's own
|
|
13
54
|
* grant. Fail closed: any error, non-200, or malformed body → null → 401.
|
|
@@ -15,41 +56,22 @@ const API_KEY_SHAPE = /^luca_(?!ort_)[a-f0-9]/i;
|
|
|
15
56
|
export function oauthResolver(env, fetchImpl = fetch) {
|
|
16
57
|
const apiBaseUrl = (env.LUCA_API_BASE_URL ?? "https://api.setluca.com").replace(TRAILING_SLASHES, "");
|
|
17
58
|
const authHeader = env.LUCA_AUTH_HEADER ?? "x-api-key";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
const payload = (await response.json());
|
|
31
|
-
if (!payload.apiKey) {
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
return {
|
|
35
|
-
config: { apiBaseUrl, apiKey: payload.apiKey, authHeader },
|
|
36
|
-
scopes: payload.scopes ?? [],
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
catch {
|
|
40
|
-
return null;
|
|
41
|
-
}
|
|
42
|
-
};
|
|
59
|
+
// Read through the same parser the API client uses, so the stdio and Worker
|
|
60
|
+
// paths cannot end up bounding their requests differently.
|
|
61
|
+
const timeoutMs = parseTimeoutMs(env.LUCA_REQUEST_TIMEOUT_MS) ?? DEFAULT_REQUEST_TIMEOUT_MS;
|
|
62
|
+
// A developer API key is another resolver's business, so it is refused here
|
|
63
|
+
// without a network call rather than sent to an endpoint that would reject it
|
|
64
|
+
// anyway.
|
|
65
|
+
return (token) => Option.match(Option.liftPredicate(token, (candidate) => !API_KEY_FIELDS.test(candidate)), {
|
|
66
|
+
onNone: () => Effect.succeed(Option.none()),
|
|
67
|
+
onSome: (candidate) => requestIdentity(apiBaseUrl, authHeader, fetchImpl, candidate, timeoutMs),
|
|
68
|
+
}).pipe(Effect.map(Option.getOrNull), Effect.runPromise);
|
|
43
69
|
}
|
|
44
70
|
/** Try resolvers in order; the first non-null identity wins. */
|
|
45
71
|
export function chainResolvers(...resolvers) {
|
|
46
|
-
return
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return null;
|
|
54
|
-
};
|
|
72
|
+
return (token) => Effect.reduce(resolvers, Option.none(), (identity, resolve) => Option.match(identity, {
|
|
73
|
+
// Once one resolver has answered, the rest are never called.
|
|
74
|
+
onSome: (present) => Effect.succeed(Option.some(present)),
|
|
75
|
+
onNone: () => Effect.promise(() => resolve(token)).pipe(Effect.map(Option.fromNullable)),
|
|
76
|
+
})).pipe(Effect.map(Option.getOrNull), Effect.runPromise);
|
|
55
77
|
}
|
package/dist/observability.js
CHANGED
|
@@ -9,11 +9,19 @@
|
|
|
9
9
|
* contract (event names + payload shape).
|
|
10
10
|
*/
|
|
11
11
|
export function logAgentSurfaceEvent(event, fields) {
|
|
12
|
-
const clean =
|
|
12
|
+
const clean = new Map([
|
|
13
|
+
["msg", "mcp.agent_surface"],
|
|
14
|
+
["event", event],
|
|
15
|
+
]);
|
|
13
16
|
for (const [k, v] of Object.entries(fields)) {
|
|
17
|
+
// Equivalent to always assigning: `JSON.stringify` omits own properties whose
|
|
18
|
+
// value is `undefined`, so guarding here vs. assigning an undefined value to
|
|
19
|
+
// the accumulator key yields byte-identical logged output. The guard keeps
|
|
20
|
+
// the object tidy.
|
|
21
|
+
// Stryker disable next-line ConditionalExpression: JSON.stringify drops undefined-valued keys regardless
|
|
14
22
|
if (v !== undefined) {
|
|
15
|
-
clean
|
|
23
|
+
clean.set(k, v);
|
|
16
24
|
}
|
|
17
25
|
}
|
|
18
|
-
console.log(JSON.stringify(clean));
|
|
26
|
+
console.log(JSON.stringify(Object.fromEntries(clean)));
|
|
19
27
|
}
|