@setluca/mcp 0.5.1 → 0.5.2

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/dist/http.js CHANGED
@@ -171,13 +171,13 @@ function sendOnce(config, input, requestIdempotencyKey, fetchImpl) {
171
171
  const now = yield* Clock.currentTimeMillis;
172
172
  const waitMs = retryAfterMs(response, now);
173
173
  const text = yield* readText.pipe(Effect.orElseSucceed(() => ""));
174
- return yield* Effect.fail(new LucaHttpError({
174
+ return yield* new LucaHttpError({
175
175
  status: response.status,
176
176
  statusText: response.statusText,
177
177
  body: yield* errorBody(text),
178
178
  ...optionalField("requestId", response.headers.get("x-request-id")),
179
179
  ...optionalField("retryAfterMs", waitMs),
180
- }));
180
+ });
181
181
  }
182
182
  const text = yield* readText;
183
183
  // An empty body is not a decode failure. Several endpoints answer with
@@ -38,7 +38,7 @@ function resolvePayload(response) {
38
38
  return Effect.tryPromise({
39
39
  try: () => response.json(),
40
40
  catch: unavailable("Token verification returned non-JSON content"),
41
- }).pipe(Effect.flatMap((body) => decodeResolvePayload(body).pipe(Effect.mapError(unavailable("Token verification returned an unexpected body")))), Effect.map(Option.some));
41
+ }).pipe(Effect.flatMap((body) => decodeResolvePayload(body).pipe(Effect.mapError(unavailable("Token verification returned an unexpected body")))), Effect.asSome);
42
42
  }
43
43
  /**
44
44
  * One authenticated call to POST /oauth/resolve, answered with the config for
@@ -115,7 +115,7 @@ export const bookingsOperations = [
115
115
  mutatesExisting: true,
116
116
  // Moves the call on the provider calendar and re-arms the lead's reminder.
117
117
  confirm: "always",
118
- description: "Reschedule or update booking status.",
118
+ description: "Reschedule or update booking status. A Google Calendar or Cal.com booking changes on that calendar too, and the calendar can email the lead about it.",
119
119
  // Each booking carries a cancellation reason the invitee may have written.
120
120
  untrustedContent: true,
121
121
  }),
@@ -126,7 +126,7 @@ export const bookingsOperations = [
126
126
  mutatesExisting: true,
127
127
  // Cancels a real call on the lead's and the coach's calendars.
128
128
  confirm: "always",
129
- description: "Cancel a booking.",
129
+ description: "Cancel a booking. A Google Calendar or Cal.com booking is cancelled on that calendar too, and the calendar can email the lead about it.",
130
130
  // Each booking carries a cancellation reason the invitee may have written.
131
131
  untrustedContent: true,
132
132
  }),
@@ -19,7 +19,7 @@ export const campaignsOperations = [
19
19
  op({
20
20
  id: "campaigns.generate",
21
21
  title: "Generate a campaign draft",
22
- description: "Generate a safe AI campaign plan without creating or publishing it.",
22
+ description: "Build a campaign plan from a brief with fixed rules, not an AI model. Nothing is created or published: create the draft from the plan, review it, and publish it yourself.",
23
23
  }),
24
24
  op({
25
25
  id: "campaigns.draft.update",
@@ -66,6 +66,8 @@ export const conversationsOperations = [
66
66
  untrustedContent: false,
67
67
  title: "Retry a voice-note transcription",
68
68
  mutatesExisting: true,
69
+ // The retried job sends the voice note to OpenRouter's speech-to-text.
70
+ openWorld: true,
69
71
  description: "Queue another transcription attempt for a voice note in this conversation. Only re-transcribes; never drafts or sends. A refusal says why: a retry is already running, the limit is reached, or the audio is gone.",
70
72
  }),
71
73
  ];
@@ -29,7 +29,7 @@ export const leadsOperations = [
29
29
  // Sends lead.updated to the coach's webhook subscribers, and a tag change
30
30
  // can resume a campaign step that messages the lead.
31
31
  openWorld: true,
32
- description: "Update a lead's display name, stage, score, tags, or custom fields.",
32
+ description: "Update a lead's display name, stage, tags, or custom fields. Tags are replaced as a whole list.",
33
33
  }),
34
34
  op({
35
35
  id: "leads.fieldDefinitions.list",
@@ -99,7 +99,13 @@ export const leadsOperations = [
99
99
  id: "leads.draft",
100
100
  title: "Draft a reply to a lead",
101
101
  untrustedContent: true,
102
- description: "Draft a reply for a lead, written the way the coach writes, and put it in the review queue for the coach to approve. It never sends, whatever the channel's auto-send setting. Optional guidance steers tone or content.",
102
+ // Drafting again replaces the lead's pending on-demand draft, as
103
+ // reviewQueue.regenerate replaces a queued one.
104
+ mutatesExisting: true,
105
+ // The drafter sends the conversation to OpenRouter's language model, the
106
+ // same reach that makes a transcription retry open-world.
107
+ openWorld: true,
108
+ description: "Draft a reply for a lead, written the way the coach writes, and put it in the review queue for the coach to approve. Drafting again replaces this lead's earlier unapproved draft from this tool. It never sends, whatever the channel's auto-send setting. Optional guidance steers tone or content.",
103
109
  }),
104
110
  op({
105
111
  id: "leads.notes.delete",
@@ -44,6 +44,9 @@ export const reviewQueueOperations = [
44
44
  untrustedContent: false,
45
45
  title: "Retry a draft's failed media",
46
46
  mutatesExisting: true,
47
+ // The same transcription job as conversations.media_retry, so the voice
48
+ // note goes to OpenRouter's speech-to-text.
49
+ openWorld: true,
47
50
  description: "Retry a failed voice-note transcription so the draft can be reviewed. Only re-transcribes; never redrafts or sends.",
48
51
  }),
49
52
  op({
@@ -19,6 +19,30 @@ const isCursor = Schema.is(NonEmptyString);
19
19
  function pageItems(page, itemsKey) {
20
20
  return Option.getOrElse(decodeItems(page[itemsKey]), () => []);
21
21
  }
22
+ const hasStringId = Schema.is(Schema.Struct({ id: Schema.String }));
23
+ /**
24
+ * A reader for the ids of the first `limit` records on a page of `operation`,
25
+ * for a caller that fans out over what a list route returned. The records sit
26
+ * under the field the route's page contract names: `items` on the review
27
+ * queue, `campaigns` on a campaign page. A page without that array, or a record
28
+ * without a string id, contributes nothing, and the caller still has the page.
29
+ *
30
+ * Throws when `operation` is not a list route. Naming one is a wiring mistake,
31
+ * and an empty id list is exactly how it would stay hidden.
32
+ */
33
+ export function recordIdsOf(operation) {
34
+ const itemsKey = operation.pageContract?.itemsKey;
35
+ if (itemsKey === undefined) {
36
+ // oxlint-disable-next-line effect/avoid-untagged-errors -- raw failure on an invariant or interop path; not a caller-recoverable domain failure
37
+ throw new Error(`${operation.id} carries no list of records to read ids from`);
38
+ }
39
+ return (page, limit) => isJsonObject(page)
40
+ ? pageItems(page, itemsKey)
41
+ .slice(0, limit)
42
+ .filter(hasStringId)
43
+ .map((record) => record.id)
44
+ : [];
45
+ }
22
46
  /**
23
47
  * The page as something the walk can merge, or nothing when the body is not an
24
48
  * object or does not carry the array the route's page shape names. Merging a
@@ -104,7 +128,7 @@ function walkPages(walk, ref) {
104
128
  // list would read as "page 3 is down", and only a failed tool result
105
129
  // carries the challenge that sends the client back through sign-in.
106
130
  if (Result.isFailure(page) && isAuthorizationFailure(page.failure)) {
107
- return yield* Effect.fail(page.failure);
131
+ return yield* page.failure;
108
132
  }
109
133
  yield* Ref.set(ref, Result.match(page, {
110
134
  onFailure: (error) => failedPage(state, formatError(error)),
package/dist/remote.js CHANGED
@@ -11,7 +11,7 @@ import { bearerChallenge } from "./oauth-challenge.js";
11
11
  import { logAgentSurfaceEvent } from "./observability.js";
12
12
  import { optionalField } from "./optional-field.js";
13
13
  import { RATE_LIMIT_WINDOW_MS, createLocalRateLimits, } from "./rate-limit.js";
14
- import { OAUTH_DEFAULT_SCOPE } from "./scopes.js";
14
+ import { OAUTH_CHALLENGE_SCOPE } from "./scopes.js";
15
15
  import { toJsonPayload } from "./serialization.js";
16
16
  import { createLucaServer } from "./server.js";
17
17
  import { sha256Hex } from "./token-verification.js";
@@ -81,13 +81,14 @@ const throttleAnonymous = (request, rateLimits) => consume(rateLimits.perAnonymo
81
81
  /**
82
82
  * A 401 with the RFC 6750 challenge. `resource_metadata` points OAuth clients
83
83
  * at the RFC 9728 document that names the authorization server. Only a request
84
- * with no token also gets the `scope` hint: a client whose token was refused
84
+ * with no token also gets the `scope` hint, which names every scope so the
85
+ * coach can choose on the consent screen: a client whose token was refused
85
86
  * must refresh it, not ask for a different scope.
86
87
  */
87
88
  function unauthorized(detail, resourceMetadataUrl, error) {
88
89
  const challenge = bearerChallenge({
89
90
  resourceMetadataUrl,
90
- ...(error === undefined ? { scope: OAUTH_DEFAULT_SCOPE } : { error }),
91
+ ...(error === undefined ? { scope: OAUTH_CHALLENGE_SCOPE } : { error }),
91
92
  });
92
93
  return jsonResponse(401, { error: "unauthorized", detail }, { "www-authenticate": challenge });
93
94
  }
package/dist/scopes.js CHANGED
@@ -40,6 +40,20 @@ export const LUCA_OAUTH_SCOPES = [
40
40
  export const LUCA_RESOURCE_SCOPES = Arr.filter(LUCA_OAUTH_SCOPES, (scope) => scope !== "offline_access");
41
41
  /** Least-privilege default for a new OAuth connection. */
42
42
  export const OAUTH_DEFAULT_SCOPE = "luca:read luca:draft";
43
+ /**
44
+ * The scope a 401 challenge asks for: every scope this resource accepts, plus
45
+ * `offline_access`. Claude and ChatGPT request exactly what the challenge
46
+ * names, and the consent screen offers only what was requested, so naming less
47
+ * would leave a coach no way to let an assistant send or book. The screen
48
+ * still starts at {@link OAUTH_DEFAULT_SCOPE}: read is locked on, draft starts
49
+ * on, and every higher scope starts unchecked for the coach to tick.
50
+ *
51
+ * `offline_access` is here for the refresh token. The authorization server
52
+ * issues one only when the grant carries it, and a client asks for no more
53
+ * than the challenge names, so without it every connection lapses when its
54
+ * one-hour access token does.
55
+ */
56
+ export const OAUTH_CHALLENGE_SCOPE = LUCA_OAUTH_SCOPES.join(" ");
43
57
  const READ_TIER_SCOPES = [
44
58
  "analytics:read",
45
59
  "bookings:read",
package/dist/server.js CHANGED
@@ -25,12 +25,30 @@ import { fromOperation, fromTaskTool, operationProgram, registerTool, } from "./
25
25
  import { TOOLSETS } from "./toolsets.js";
26
26
  import { LUCA_MCP_VERSION } from "./version.js";
27
27
  const LucaLive = LucaApiLive.pipe(Layer.provide(LucaConfigLive));
28
+ /**
29
+ * SetLuca's icons, for the Worker's server card. They stay out of
30
+ * {@link LUCA_SERVER_INFO}: claude.ai abandons a session whose `initialize`
31
+ * answer carries `serverInfo.icons`, at any protocol version
32
+ * (anthropics/claude-ai-mcp#474, open). The directories take their icon from
33
+ * the listing upload, not the handshake, so leaving them out costs nothing.
34
+ */
35
+ export const LUCA_SERVER_ICONS = [
36
+ {
37
+ src: "https://setluca.com/favicon.svg?v=wordmark-1",
38
+ mimeType: "image/svg+xml",
39
+ },
40
+ {
41
+ src: "https://setluca.com/android-chrome-512x512.png?v=wordmark-1",
42
+ mimeType: "image/png",
43
+ sizes: ["512x512"],
44
+ },
45
+ ];
28
46
  /**
29
47
  * How this server identifies itself to a client. Protocol revision 2026-07-28
30
- * carries a title, description, website, and icons alongside the name and
31
- * version, and a client renders those in its server list instead of the bare
32
- * package name. The Worker's server card is built from the same values, so
33
- * the two descriptions of this server cannot drift apart.
48
+ * carries a title, description, and website alongside the name and version,
49
+ * and a client renders those in its server list instead of the bare package
50
+ * name. The Worker's server card is built from the same values, so the two
51
+ * descriptions of this server cannot drift apart.
34
52
  */
35
53
  export const LUCA_SERVER_INFO = {
36
54
  name: "luca-mcp",
@@ -40,17 +58,6 @@ export const LUCA_SERVER_INFO = {
40
58
  // live and says which one is not.
41
59
  description: "SetLuca runs DM sales for coaches, creators, and experts. Work with leads, conversations, drafted replies, campaigns, broadcasts, and booked calls on Telegram, Instagram, and Messenger. WhatsApp is coming soon.",
42
60
  websiteUrl: "https://setluca.com",
43
- icons: [
44
- {
45
- src: "https://setluca.com/favicon.svg?v=wordmark-1",
46
- mimeType: "image/svg+xml",
47
- },
48
- {
49
- src: "https://setluca.com/android-chrome-512x512.png?v=wordmark-1",
50
- mimeType: "image/png",
51
- sizes: ["512x512"],
52
- },
53
- ],
54
61
  };
55
62
  /**
56
63
  * What a client hands its model at connection time: where to start and which
@@ -6,10 +6,13 @@ import * as Result from "effect/Result";
6
6
  import * as Schema from "effect/Schema";
7
7
  import { formatError } from "../errors.js";
8
8
  import { NonEmptyString, WORKSPACE_INPUT_FIELDS } from "../fields.js";
9
+ import { operationById } from "../operations.js";
9
10
  import { optionalField } from "../optional-field.js";
10
- import { failureAsField, listingIds, numberInput, operationResult, resultOrError, sectionResult, taskTool, } from "./runtime.js";
11
+ import { recordIdsOf } from "../pagination.js";
12
+ import { failureAsField, numberInput, operationResult, resultOrError, sectionResult, taskTool, } from "./runtime.js";
11
13
  const DEFAULT_CAMPAIGN_LIMIT = 5;
12
14
  const CAMPAIGN_BROADCAST_LIMIT = 10;
15
+ const campaignIds = recordIdsOf(operationById("campaigns.list"));
13
16
  const ANALYTICS_PERIOD_VALUES = [
14
17
  "this_week",
15
18
  "this_month",
@@ -130,7 +133,7 @@ function analyticsFanOut(call, period, reads) {
130
133
  * {@link failureAsField}.
131
134
  */
132
135
  function campaignAnalytics(call, page, limit) {
133
- return Effect.forEach(listingIds(page, limit), (id) => failureAsField("analyticsError", call("campaigns.analytics", { pathParams: { id } })).pipe(Effect.map((analytics) => [id, analytics])),
136
+ return Effect.forEach(campaignIds(page, limit), (id) => failureAsField("analyticsError", call("campaigns.analytics", { pathParams: { id } })).pipe(Effect.map((analytics) => [id, analytics])),
134
137
  // Effect.forEach also defaults to concurrency 1. The limit caps this at 5
135
138
  // independent per-campaign lookups, so unbounded is fine.
136
139
  { concurrency: "unbounded" }).pipe(Effect.map(R.fromEntries));
@@ -239,7 +239,7 @@ const DEFAULT_GUEST_LINK_HOURS = 168;
239
239
  const rescheduleCall = taskTool({
240
240
  name: "luca_reschedule_call",
241
241
  title: "Move a call to a new time",
242
- description: "Move a booked call to a new time and hand back a fresh reschedule link for the lead. Checks the new slot is open first, against the schedule of its booking type when it has one, and returns the open slots instead of moving the call when it is taken. A call with an idempotencyKey skips that check so a retry gets the original result back. A manual booking moves in Luca only. A Cal.com or Google Calendar booking moves on that calendar too, but only once the provider confirms: until then the booking's operation.state reads pending_provider and it keeps its old time, so read the booking again before telling the coach the call moved. A Calendly, GoHighLevel or iClosed booking is refused so the coach can move it with the provider, and so is a provider booking with no calendar event, such as one recorded with luca_bookings_create. Moving the call re-arms the lead's reminder for the new time, so pass confirm: true only after the coach approves the move. Example: call with { bookingId, slotAt: \"2026-07-16T15:00:00Z\", confirm: true } after the lead asked to push the call. No message about the move is sent: send the lead the link yourself, or draft the message through the review queue.",
242
+ description: "Move a booked call to a new time and hand back a fresh reschedule link for the lead. Checks the new slot is open first, against the schedule of its booking type when it has one, and returns the open slots instead of moving the call when it is taken. A call with an idempotencyKey skips that check so a retry gets the original result back. A manual booking moves in SetLuca only. A Cal.com or Google Calendar booking moves on that calendar too, but only once the provider confirms: until then the booking's operation.state reads pending_provider and it keeps its old time, so read the booking again before telling the coach the call moved. A Calendly, GoHighLevel or iClosed booking is refused so the coach can move it with the provider, and so is a provider booking with no calendar event, such as one recorded with luca_bookings_create. Moving the call re-arms the lead's reminder for the new time, so pass confirm: true only after the coach approves the move. Example: call with { bookingId, slotAt: \"2026-07-16T15:00:00Z\", confirm: true } after the lead asked to push the call. SetLuca sends the lead no DM about the move, but a Google Calendar booking's calendar emails the lead the updated invite, and Cal.com may too. Send the lead the link yourself, or draft the message through the review queue.",
243
243
  inputSchema: {
244
244
  ...WORKSPACE_INPUT_FIELDS,
245
245
  bookingId: NonEmptyString.annotate({ description: "The booking to move." }),
@@ -282,7 +282,7 @@ const rescheduleCall = taskTool({
282
282
  ...window,
283
283
  bookingTypeId,
284
284
  timezone: "UTC",
285
- }).pipe(Effect.map(Option.some)),
285
+ }).pipe(Effect.asSome),
286
286
  })));
287
287
  if (Option.isSome(open) &&
288
288
  !open.value.some((candidate) => sameInstant(candidate, slotAt))) {
@@ -5,9 +5,12 @@ import * as Schema from "effect/Schema";
5
5
  import { formatError } from "../errors.js";
6
6
  import { WORKSPACE_INPUT_FIELDS } from "../fields.js";
7
7
  import { logAgentSurfaceEvent } from "../observability.js";
8
- import { isPartialFailure, listingIds, numberInput, operationResult, resultOrError, taskTool, } from "./runtime.js";
8
+ import { operationById } from "../operations.js";
9
+ import { recordIdsOf } from "../pagination.js";
10
+ import { isPartialFailure, numberInput, operationResult, resultOrError, taskTool, } from "./runtime.js";
9
11
  const DEFAULT_QUEUE_LIMIT = 20;
10
12
  const DEFAULT_EXPLAIN_TOP = 3;
13
+ const queueIds = recordIdsOf(operationById("reviewQueue.list"));
11
14
  const TRIAGE_INBOX_TOOL_NAME = "luca_triage_inbox";
12
15
  /**
13
16
  * One explanation, with a failed lookup carried in the result instead of
@@ -79,7 +82,7 @@ const triageInbox = taskTool({
79
82
  const queue = yield* call("reviewQueue.list", {
80
83
  query: { limit: numberInput(input.limit, DEFAULT_QUEUE_LIMIT) },
81
84
  });
82
- const ids = listingIds(queue, numberInput(input.explainTop, DEFAULT_EXPLAIN_TOP));
85
+ const ids = queueIds(queue, numberInput(input.explainTop, DEFAULT_EXPLAIN_TOP));
83
86
  const attempts = yield* Effect.forEach(ids, (id) => explainOne(call, id),
84
87
  // Effect.forEach defaults to concurrency 1 (sequential). `explainTop`
85
88
  // caps this fan-out at 5 independent lookups, so unbounded is fine.
@@ -48,7 +48,7 @@ const flagForHuman = taskTool({
48
48
  const draftReply = taskTool({
49
49
  name: "luca_draft_reply",
50
50
  title: "Draft a reply for a lead",
51
- description: 'Draft a reply for a lead, written the way the coach writes, and put it in the SetLuca review queue, where it waits for the coach to approve it. It never sends, whatever the channel\'s auto-send setting. Optional guidance steers tone or content. Example: call with { leadId, guidance: "keep it short and warm" }.',
51
+ description: "Draft a reply for a lead, written the way the coach writes, and put it in the SetLuca review queue, where it waits for the coach to approve it. Drafting again replaces this lead's earlier unapproved draft from this tool. It never sends, whatever the channel's auto-send setting. Optional guidance steers tone or content. Example: call with { leadId, guidance: \"keep it short and warm\" }.",
52
52
  inputSchema: {
53
53
  ...WORKSPACE_INPUT_FIELDS,
54
54
  leadId: NonEmptyString.annotate({
@@ -63,38 +63,6 @@ export function taskRequest(operation, input, parts = {}) {
63
63
  ...optionalField("workspace", workspaceOverride(input)),
64
64
  };
65
65
  }
66
- /**
67
- * The only part of a listing page a composing tool reads for itself: the ids of
68
- * the items it then fans out over. Decoding that alone leaves the raw page
69
- * intact for the caller, returned exactly as the API sent it, while the ids the
70
- * tool acts on are validated rather than asserted.
71
- */
72
- const ListingIds = Schema.Struct({
73
- items: Schema.optional(Schema.Array(Schema.Struct({ id: Schema.optional(Schema.String) }))),
74
- });
75
- const decodeListingIds = Schema.decodeUnknownOption(ListingIds);
76
- /**
77
- * Ids of the first `limit` items on a listing page. A page whose fields we
78
- * cannot read, or whose items carry no id, contributes nothing. The tool still
79
- * returns the page itself rather than failing over a fan-out it cannot do.
80
- */
81
- export function listingIds(page, limit) {
82
- // Both fallbacks below are empty arrays, and substituting a one-item array
83
- // for either is an equivalent mutation rather than an untested branch: a
84
- // substituted item carries no `id`, so the `filter` at the end of this
85
- // function drops it and the result is the same empty id list. Substituting
86
- // `undefined` for the outer fallback is a real change and is covered by the
87
- // undecodable-page test.
88
- const items = decodeListingIds(page).pipe(
89
- // Stryker disable next-line ArrayDeclaration: a substituted item has no id and is filtered out below
90
- Option.map((decoded) => decoded.items ?? []),
91
- // Stryker disable next-line ArrayDeclaration: a substituted item has no id and is filtered out below
92
- Option.getOrElse(() => []));
93
- return items
94
- .slice(0, limit)
95
- .map((item) => item.id)
96
- .filter((id) => id !== undefined);
97
- }
98
66
  /**
99
67
  * A request body carrying a single text field, or no body at all when the input
100
68
  * is absent or blank. Sending an empty string would mean something different to
package/dist/toolsets.js CHANGED
@@ -1,8 +1,8 @@
1
1
  import { LUCA_TASK_TOOLS } from "./task-tools.js";
2
2
  /**
3
- * The tools the `claude` toolset exposes. This list is the contract with the
4
- * connector directory submission, and a test pins it name for name. Adding or
5
- * removing a tool here changes what the directory lists.
3
+ * The tools the `claude` toolset exposes, and a test pins it name for name.
4
+ * Coaches who chose `?toolset=claude` rely on it staying read and draft, so a
5
+ * tool that sends, books, or changes settings does not belong here.
6
6
  */
7
7
  export const CLAUDE_TOOL_NAMES = [
8
8
  "luca_triage_inbox",
package/dist/version.js CHANGED
@@ -9,6 +9,6 @@
9
9
  *
10
10
  * Bump this in the same commit as package.json and server.json.
11
11
  */
12
- export const LUCA_MCP_VERSION = "0.5.1";
12
+ export const LUCA_MCP_VERSION = "0.5.2";
13
13
  /** The user-agent every outbound Luca API call identifies itself with. */
14
14
  export const LUCA_MCP_USER_AGENT = `luca-mcp/${LUCA_MCP_VERSION}`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@setluca/mcp",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "description": "Connect SetLuca to Claude, ChatGPT, Cursor, or any MCP client to triage the review queue, look up leads, draft replies, and check campaigns.",
6
6
  "keywords": [
@@ -68,15 +68,17 @@
68
68
  "registry:check": "bun scripts/check-registry.ts",
69
69
  "docs:generate": "bun scripts/generate-docs.ts --write",
70
70
  "docs:check": "bun scripts/generate-docs.ts --check",
71
- "clean": "rm -rf .turbo dist coverage node_modules"
71
+ "clean": "rm -rf .turbo dist coverage node_modules",
72
+ "prepare": "effect-tsgo patch --typescript --no-oxlint"
72
73
  },
73
74
  "dependencies": {
74
75
  "@modelcontextprotocol/server": "^2.0.0",
75
- "effect": "4.0.0"
76
+ "effect": "4.0.6"
76
77
  },
77
78
  "devDependencies": {
78
- "@effect/platform-bun": "4.0.0",
79
- "@effect/vitest": "4.0.0",
79
+ "@effect/platform-bun": "4.0.6",
80
+ "@effect/tsgo": "0.56.0",
81
+ "@effect/vitest": "4.0.6",
80
82
  "@modelcontextprotocol/client": "^2.0.0",
81
83
  "@modelcontextprotocol/inspector": "^2.5.0",
82
84
  "@mpsuesser/oxlint-plugin-effect": "0.6.0",