skedyul 1.7.7 → 1.7.9

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/server.js CHANGED
@@ -1988,6 +1988,39 @@ async function handleMcpBatchRoute(req, ctx) {
1988
1988
  }
1989
1989
  };
1990
1990
  }
1991
+ function isBatchOperationFailure(result) {
1992
+ return result != null && typeof result === "object" && result.success === false && typeof result.error === "object" && result.error != null;
1993
+ }
1994
+ function toStringEnv(env) {
1995
+ const out = {};
1996
+ for (const [key, value] of Object.entries(env)) {
1997
+ if (value !== void 0) {
1998
+ out[key] = value;
1999
+ }
2000
+ }
2001
+ return out;
2002
+ }
2003
+ function stripBillingFields(result) {
2004
+ const {
2005
+ billing: _billing,
2006
+ success: _success,
2007
+ error: _error,
2008
+ retry: _retry,
2009
+ ...domain
2010
+ } = result;
2011
+ return domain;
2012
+ }
2013
+ function softFailureResponse(error, retry, billing) {
2014
+ return {
2015
+ status: 200,
2016
+ body: {
2017
+ success: false,
2018
+ error,
2019
+ ...retry ? { retry } : {},
2020
+ billing: normalizeBilling(billing)
2021
+ }
2022
+ };
2023
+ }
1991
2024
  async function handleBatchOperationRoute(req, ctx) {
1992
2025
  const bodyResult = parseJsonBody(req);
1993
2026
  if (!bodyResult.success) {
@@ -1996,34 +2029,80 @@ async function handleBatchOperationRoute(req, ctx) {
1996
2029
  const body = bodyResult.data;
1997
2030
  const registry = ctx.batchOperationRegistry;
1998
2031
  if (!registry) {
1999
- return {
2000
- status: 404,
2001
- body: {
2002
- success: false,
2003
- error: "No batch operations registered"
2004
- }
2005
- };
2032
+ return softFailureResponse({
2033
+ code: "NOT_FOUND",
2034
+ message: "No batch operations registered",
2035
+ category: "validation"
2036
+ });
2006
2037
  }
2007
2038
  const operation = registry[body.handle];
2008
2039
  if (!operation) {
2009
- return {
2010
- status: 404,
2011
- body: {
2012
- success: false,
2013
- error: `Batch operation not found: ${body.handle}`
2014
- }
2015
- };
2040
+ return softFailureResponse({
2041
+ code: "NOT_FOUND",
2042
+ message: `Batch operation not found: ${body.handle}`,
2043
+ category: "validation"
2044
+ });
2016
2045
  }
2017
- const log = {
2018
- info: (message, meta) => {
2019
- console.log(`[batch:${body.handle}] ${message}`, meta ?? "");
2020
- },
2021
- warn: (message, meta) => {
2022
- console.warn(`[batch:${body.handle}] ${message}`, meta ?? "");
2023
- },
2024
- error: (message, meta) => {
2025
- console.error(`[batch:${body.handle}] ${message}`, meta ?? "");
2026
- }
2046
+ const context = body.context;
2047
+ if (!context?.workplaceId || !context?.appInstallationId || !context?.appId) {
2048
+ return softFailureResponse({
2049
+ code: "VALIDATION_ERROR",
2050
+ message: "Missing context (workplaceId, appInstallationId, and appId required)",
2051
+ category: "validation"
2052
+ });
2053
+ }
2054
+ if (body.method !== "setup" && body.method !== "iterate") {
2055
+ return softFailureResponse({
2056
+ code: "VALIDATION_ERROR",
2057
+ message: `Unknown method: ${body.method}`,
2058
+ category: "validation"
2059
+ });
2060
+ }
2061
+ const requestEnv = body.env ?? {};
2062
+ const mergedEnv = toStringEnv(buildToolExecutionEnv(requestEnv));
2063
+ const originalEnv = { ...process.env };
2064
+ Object.assign(process.env, mergedEnv);
2065
+ const log = createContextLogger();
2066
+ const app = context.app ?? {
2067
+ id: context.appId,
2068
+ versionId: context.appId
2069
+ };
2070
+ const workplace = context.workplace ?? {
2071
+ id: context.workplaceId,
2072
+ subdomain: ""
2073
+ };
2074
+ const invocation = body.invocation ?? {
2075
+ invocationId: `${body.handle}-${body.method}-${Date.now()}`,
2076
+ invocationType: "batch_operation",
2077
+ batchOperationHandle: body.handle,
2078
+ batchOperationMethod: body.method,
2079
+ appInstallationId: context.appInstallationId
2080
+ };
2081
+ const requestConfig = {
2082
+ baseUrl: mergedEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2083
+ apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2084
+ };
2085
+ const rateLimitContext = {
2086
+ app,
2087
+ appInstallationId: context.appInstallationId,
2088
+ invocation,
2089
+ preAcquiredLeases: parsePreAcquiredLeases(
2090
+ mergedEnv.SKEDYUL_RATE_LIMIT_LEASES
2091
+ ),
2092
+ heldMutexQueueKeys: parseHeldMutexQueueKeys(
2093
+ mergedEnv.SKEDYUL_HELD_MUTEX_QUEUE_KEYS
2094
+ )
2095
+ };
2096
+ const opContextBase = {
2097
+ workplaceId: context.workplaceId,
2098
+ appInstallationId: context.appInstallationId,
2099
+ appId: context.appId,
2100
+ app,
2101
+ workplace,
2102
+ input: body.input,
2103
+ env: mergedEnv,
2104
+ invocation,
2105
+ log
2027
2106
  };
2028
2107
  try {
2029
2108
  if (body.method === "setup") {
@@ -2032,61 +2111,88 @@ async function handleBatchOperationRoute(req, ctx) {
2032
2111
  status: 200,
2033
2112
  body: {
2034
2113
  success: true,
2035
- result: { state: {}, total: void 0 }
2114
+ result: { state: {}, total: void 0 },
2115
+ billing: normalizeBilling(void 0)
2036
2116
  }
2037
2117
  };
2038
2118
  }
2039
- const result = await operation.setup({
2040
- workplaceId: body.workplaceId,
2041
- appInstallationId: body.appInstallationId,
2042
- appId: body.appId,
2043
- input: body.input,
2044
- log
2119
+ const raw2 = await runWithConfig(requestConfig, async () => {
2120
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
2121
+ return runWithLogContext({ invocation }, async () => {
2122
+ return operation.setup(opContextBase);
2123
+ });
2124
+ });
2045
2125
  });
2126
+ if (isBatchOperationFailure(raw2)) {
2127
+ return softFailureResponse(raw2.error, raw2.retry, raw2.billing);
2128
+ }
2129
+ const setupResult = raw2;
2130
+ const domain2 = stripBillingFields(
2131
+ setupResult
2132
+ );
2046
2133
  return {
2047
2134
  status: 200,
2048
2135
  body: {
2049
2136
  success: true,
2050
- result
2137
+ result: domain2,
2138
+ billing: normalizeBilling(setupResult.billing)
2051
2139
  }
2052
2140
  };
2053
2141
  }
2054
- if (body.method === "iterate") {
2055
- const result = await operation.iterate({
2056
- workplaceId: body.workplaceId,
2057
- appInstallationId: body.appInstallationId,
2058
- appId: body.appId,
2059
- input: body.input,
2060
- state: body.state,
2061
- page: body.page,
2062
- cursor: body.cursor,
2063
- limit: body.limit,
2064
- log
2142
+ const raw = await runWithConfig(requestConfig, async () => {
2143
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
2144
+ return runWithLogContext({ invocation }, async () => {
2145
+ return operation.iterate({
2146
+ ...opContextBase,
2147
+ state: body.state,
2148
+ page: body.page,
2149
+ cursor: body.cursor,
2150
+ limit: body.limit
2151
+ });
2152
+ });
2065
2153
  });
2066
- return {
2067
- status: 200,
2068
- body: {
2069
- success: true,
2070
- result
2071
- }
2072
- };
2154
+ });
2155
+ if (isBatchOperationFailure(raw)) {
2156
+ return softFailureResponse(raw.error, raw.retry, raw.billing);
2073
2157
  }
2158
+ const iterateResult = raw;
2159
+ const domain = stripBillingFields(
2160
+ iterateResult
2161
+ );
2074
2162
  return {
2075
- status: 400,
2163
+ status: 200,
2076
2164
  body: {
2077
- success: false,
2078
- error: `Unknown method: ${body.method}`
2165
+ success: true,
2166
+ result: domain,
2167
+ billing: normalizeBilling(iterateResult.billing)
2079
2168
  }
2080
2169
  };
2081
2170
  } catch (err) {
2082
- log.error("Batch operation failed", { error: err });
2171
+ if (err instanceof RateLimitExceededError) {
2172
+ return softFailureResponse(
2173
+ {
2174
+ code: "RATE_LIMITED",
2175
+ message: err.message,
2176
+ category: "external"
2177
+ },
2178
+ { allowed: true, afterMs: err.retryAfterMs }
2179
+ );
2180
+ }
2181
+ log.error("Batch operation failed", err);
2083
2182
  return {
2084
2183
  status: 500,
2085
2184
  body: {
2086
2185
  success: false,
2087
- error: err instanceof Error ? err.message : String(err)
2186
+ error: {
2187
+ code: "INTERNAL_ERROR",
2188
+ message: err instanceof Error ? err.message : String(err),
2189
+ category: "internal"
2190
+ },
2191
+ billing: normalizeBilling(void 0)
2088
2192
  }
2089
2193
  };
2194
+ } finally {
2195
+ process.env = originalEnv;
2090
2196
  }
2091
2197
  }
2092
2198
  function createNotFoundResponse() {
@@ -1949,6 +1949,39 @@ async function handleMcpBatchRoute(req, ctx) {
1949
1949
  }
1950
1950
  };
1951
1951
  }
1952
+ function isBatchOperationFailure(result) {
1953
+ return result != null && typeof result === "object" && result.success === false && typeof result.error === "object" && result.error != null;
1954
+ }
1955
+ function toStringEnv(env) {
1956
+ const out = {};
1957
+ for (const [key, value] of Object.entries(env)) {
1958
+ if (value !== void 0) {
1959
+ out[key] = value;
1960
+ }
1961
+ }
1962
+ return out;
1963
+ }
1964
+ function stripBillingFields(result) {
1965
+ const {
1966
+ billing: _billing,
1967
+ success: _success,
1968
+ error: _error,
1969
+ retry: _retry,
1970
+ ...domain
1971
+ } = result;
1972
+ return domain;
1973
+ }
1974
+ function softFailureResponse(error, retry, billing) {
1975
+ return {
1976
+ status: 200,
1977
+ body: {
1978
+ success: false,
1979
+ error,
1980
+ ...retry ? { retry } : {},
1981
+ billing: normalizeBilling(billing)
1982
+ }
1983
+ };
1984
+ }
1952
1985
  async function handleBatchOperationRoute(req, ctx) {
1953
1986
  const bodyResult = parseJsonBody(req);
1954
1987
  if (!bodyResult.success) {
@@ -1957,34 +1990,80 @@ async function handleBatchOperationRoute(req, ctx) {
1957
1990
  const body = bodyResult.data;
1958
1991
  const registry = ctx.batchOperationRegistry;
1959
1992
  if (!registry) {
1960
- return {
1961
- status: 404,
1962
- body: {
1963
- success: false,
1964
- error: "No batch operations registered"
1965
- }
1966
- };
1993
+ return softFailureResponse({
1994
+ code: "NOT_FOUND",
1995
+ message: "No batch operations registered",
1996
+ category: "validation"
1997
+ });
1967
1998
  }
1968
1999
  const operation = registry[body.handle];
1969
2000
  if (!operation) {
1970
- return {
1971
- status: 404,
1972
- body: {
1973
- success: false,
1974
- error: `Batch operation not found: ${body.handle}`
1975
- }
1976
- };
2001
+ return softFailureResponse({
2002
+ code: "NOT_FOUND",
2003
+ message: `Batch operation not found: ${body.handle}`,
2004
+ category: "validation"
2005
+ });
1977
2006
  }
1978
- const log = {
1979
- info: (message, meta) => {
1980
- console.log(`[batch:${body.handle}] ${message}`, meta ?? "");
1981
- },
1982
- warn: (message, meta) => {
1983
- console.warn(`[batch:${body.handle}] ${message}`, meta ?? "");
1984
- },
1985
- error: (message, meta) => {
1986
- console.error(`[batch:${body.handle}] ${message}`, meta ?? "");
1987
- }
2007
+ const context = body.context;
2008
+ if (!context?.workplaceId || !context?.appInstallationId || !context?.appId) {
2009
+ return softFailureResponse({
2010
+ code: "VALIDATION_ERROR",
2011
+ message: "Missing context (workplaceId, appInstallationId, and appId required)",
2012
+ category: "validation"
2013
+ });
2014
+ }
2015
+ if (body.method !== "setup" && body.method !== "iterate") {
2016
+ return softFailureResponse({
2017
+ code: "VALIDATION_ERROR",
2018
+ message: `Unknown method: ${body.method}`,
2019
+ category: "validation"
2020
+ });
2021
+ }
2022
+ const requestEnv = body.env ?? {};
2023
+ const mergedEnv = toStringEnv(buildToolExecutionEnv(requestEnv));
2024
+ const originalEnv = { ...process.env };
2025
+ Object.assign(process.env, mergedEnv);
2026
+ const log = createContextLogger();
2027
+ const app = context.app ?? {
2028
+ id: context.appId,
2029
+ versionId: context.appId
2030
+ };
2031
+ const workplace = context.workplace ?? {
2032
+ id: context.workplaceId,
2033
+ subdomain: ""
2034
+ };
2035
+ const invocation = body.invocation ?? {
2036
+ invocationId: `${body.handle}-${body.method}-${Date.now()}`,
2037
+ invocationType: "batch_operation",
2038
+ batchOperationHandle: body.handle,
2039
+ batchOperationMethod: body.method,
2040
+ appInstallationId: context.appInstallationId
2041
+ };
2042
+ const requestConfig = {
2043
+ baseUrl: mergedEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
2044
+ apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
2045
+ };
2046
+ const rateLimitContext = {
2047
+ app,
2048
+ appInstallationId: context.appInstallationId,
2049
+ invocation,
2050
+ preAcquiredLeases: parsePreAcquiredLeases(
2051
+ mergedEnv.SKEDYUL_RATE_LIMIT_LEASES
2052
+ ),
2053
+ heldMutexQueueKeys: parseHeldMutexQueueKeys(
2054
+ mergedEnv.SKEDYUL_HELD_MUTEX_QUEUE_KEYS
2055
+ )
2056
+ };
2057
+ const opContextBase = {
2058
+ workplaceId: context.workplaceId,
2059
+ appInstallationId: context.appInstallationId,
2060
+ appId: context.appId,
2061
+ app,
2062
+ workplace,
2063
+ input: body.input,
2064
+ env: mergedEnv,
2065
+ invocation,
2066
+ log
1988
2067
  };
1989
2068
  try {
1990
2069
  if (body.method === "setup") {
@@ -1993,61 +2072,88 @@ async function handleBatchOperationRoute(req, ctx) {
1993
2072
  status: 200,
1994
2073
  body: {
1995
2074
  success: true,
1996
- result: { state: {}, total: void 0 }
2075
+ result: { state: {}, total: void 0 },
2076
+ billing: normalizeBilling(void 0)
1997
2077
  }
1998
2078
  };
1999
2079
  }
2000
- const result = await operation.setup({
2001
- workplaceId: body.workplaceId,
2002
- appInstallationId: body.appInstallationId,
2003
- appId: body.appId,
2004
- input: body.input,
2005
- log
2080
+ const raw2 = await runWithConfig(requestConfig, async () => {
2081
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
2082
+ return runWithLogContext({ invocation }, async () => {
2083
+ return operation.setup(opContextBase);
2084
+ });
2085
+ });
2006
2086
  });
2087
+ if (isBatchOperationFailure(raw2)) {
2088
+ return softFailureResponse(raw2.error, raw2.retry, raw2.billing);
2089
+ }
2090
+ const setupResult = raw2;
2091
+ const domain2 = stripBillingFields(
2092
+ setupResult
2093
+ );
2007
2094
  return {
2008
2095
  status: 200,
2009
2096
  body: {
2010
2097
  success: true,
2011
- result
2098
+ result: domain2,
2099
+ billing: normalizeBilling(setupResult.billing)
2012
2100
  }
2013
2101
  };
2014
2102
  }
2015
- if (body.method === "iterate") {
2016
- const result = await operation.iterate({
2017
- workplaceId: body.workplaceId,
2018
- appInstallationId: body.appInstallationId,
2019
- appId: body.appId,
2020
- input: body.input,
2021
- state: body.state,
2022
- page: body.page,
2023
- cursor: body.cursor,
2024
- limit: body.limit,
2025
- log
2103
+ const raw = await runWithConfig(requestConfig, async () => {
2104
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
2105
+ return runWithLogContext({ invocation }, async () => {
2106
+ return operation.iterate({
2107
+ ...opContextBase,
2108
+ state: body.state,
2109
+ page: body.page,
2110
+ cursor: body.cursor,
2111
+ limit: body.limit
2112
+ });
2113
+ });
2026
2114
  });
2027
- return {
2028
- status: 200,
2029
- body: {
2030
- success: true,
2031
- result
2032
- }
2033
- };
2115
+ });
2116
+ if (isBatchOperationFailure(raw)) {
2117
+ return softFailureResponse(raw.error, raw.retry, raw.billing);
2034
2118
  }
2119
+ const iterateResult = raw;
2120
+ const domain = stripBillingFields(
2121
+ iterateResult
2122
+ );
2035
2123
  return {
2036
- status: 400,
2124
+ status: 200,
2037
2125
  body: {
2038
- success: false,
2039
- error: `Unknown method: ${body.method}`
2126
+ success: true,
2127
+ result: domain,
2128
+ billing: normalizeBilling(iterateResult.billing)
2040
2129
  }
2041
2130
  };
2042
2131
  } catch (err) {
2043
- log.error("Batch operation failed", { error: err });
2132
+ if (err instanceof RateLimitExceededError) {
2133
+ return softFailureResponse(
2134
+ {
2135
+ code: "RATE_LIMITED",
2136
+ message: err.message,
2137
+ category: "external"
2138
+ },
2139
+ { allowed: true, afterMs: err.retryAfterMs }
2140
+ );
2141
+ }
2142
+ log.error("Batch operation failed", err);
2044
2143
  return {
2045
2144
  status: 500,
2046
2145
  body: {
2047
2146
  success: false,
2048
- error: err instanceof Error ? err.message : String(err)
2147
+ error: {
2148
+ code: "INTERNAL_ERROR",
2149
+ message: err instanceof Error ? err.message : String(err),
2150
+ category: "internal"
2151
+ },
2152
+ billing: normalizeBilling(void 0)
2049
2153
  }
2050
2154
  };
2155
+ } finally {
2156
+ process.env = originalEnv;
2051
2157
  }
2052
2158
  }
2053
2159
  function createNotFoundResponse() {
@@ -6,7 +6,7 @@
6
6
  */
7
7
  export type { AppInfo, AppInfoWithHandles, WorkplaceInfo, RequestInfo } from './shared';
8
8
  export type { InvocationType, ServerHookHandle, InvocationContext, } from './invocation';
9
- export { createToolCallContext, createServerHookContext, createWebhookContext, createWorkflowStepContext, } from './invocation';
9
+ export { createToolCallContext, createServerHookContext, createWebhookContext, createWorkflowStepContext, createBatchOperationContext, } from './invocation';
10
10
  export type { ToolTrigger, ProvisionToolContext, DeveloperPageActionToolContext, DeveloperFormSubmitToolContext, FieldChangeToolContext, PageActionToolContext, FormSubmitToolContext, AgentToolContext, WorkflowToolContext, CronToolContext, CronContext, ToolExecutionContext, } from './tool-context';
11
11
  export { isProvisionContext, isRuntimeContext, isCronContext, isDeveloperContext } from './tool-context';
12
12
  export type { ToolResult, ToolSuccess, ToolFailure, ErrorCode, ErrorCategory, ToolWarning, ToolPagination, ToolBilling, ToolRetry, ToolTiming, ToolCompletionHints, ToolConfig, QueueTouchPoint, BillingInfo, ToolResponseMeta, ToolEffect, ToolError, ToolExecutionResult, ToolSchemaWithJson, ToolSchema, ToolHandler, ToolDefinition, ToolRegistryEntry, ToolRegistry, ToolName, ToolMetadata, ToolCallResponse, ExecutionScope, } from './tool';
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * Type of invocation that triggered the executable
3
3
  */
4
- export type InvocationType = 'tool_call' | 'server_hook' | 'workflow_step' | 'webhook';
4
+ export type InvocationType = 'tool_call' | 'server_hook' | 'workflow_step' | 'webhook' | 'batch_operation';
5
5
  /**
6
6
  * Server hook handles for lifecycle events
7
7
  */
@@ -19,6 +19,10 @@ export interface InvocationContext {
19
19
  toolCallId?: string;
20
20
  /** The tool's handle for searching (e.g., "get_customers") */
21
21
  toolHandle?: string;
22
+ /** Batch operation handle (for batch_operation invocations) */
23
+ batchOperationHandle?: string;
24
+ /** setup | iterate */
25
+ batchOperationMethod?: 'setup' | 'iterate';
22
26
  /** Hook type: "provision", "install", "uninstall", "oauth_callback", "setup.revalidate" */
23
27
  serverHookHandle?: ServerHookHandle;
24
28
  /** The app installation that triggered the call */
@@ -69,3 +73,14 @@ export declare function createWorkflowStepContext(params: {
69
73
  workflowRunId: string;
70
74
  workflowStepId?: string;
71
75
  }): InvocationContext;
76
+ /**
77
+ * Create an invocation context for a batch operation setup/iterate call
78
+ */
79
+ export declare function createBatchOperationContext(params: {
80
+ invocationId: string;
81
+ batchOperationHandle: string;
82
+ batchOperationMethod: 'setup' | 'iterate';
83
+ appInstallationId?: string;
84
+ workflowId?: string;
85
+ workflowRunId?: string;
86
+ }): InvocationContext;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skedyul",
3
- "version": "1.7.7",
3
+ "version": "1.7.9",
4
4
  "description": "The Skedyul SDK for Node.js",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -76,7 +76,7 @@
76
76
  "build": "rm -rf dist tsconfig.tsbuildinfo && tsc --emitDeclarationOnly && pnpm exec tsup-node && node -e \"require('fs').mkdirSync('dist',{recursive:true}); require('fs').writeFileSync('dist/.build-stamp', String(Date.now()))\"",
77
77
  "db:generate": "echo 'No database generation needed for this package'",
78
78
  "dev:watch": "npx nodemon --watch src --ext js,ts --exec \"pnpm run build\"",
79
- "test": "tsc --project tsconfig.tests.json && node --test dist-tests/tests/server.test.js dist-tests/tests/cli.test.js dist-tests/tests/agent-schema-v3.test.js dist-tests/tests/ratelimit.test.js dist-tests/tests/sequencer.test.js dist-tests/tests/config-loader.test.js dist-tests/tests/gsm7.test.js"
79
+ "test": "tsc --project tsconfig.tests.json && node --test dist-tests/tests/server.test.js dist-tests/tests/cli.test.js dist-tests/tests/agent-schema-v3.test.js dist-tests/tests/ratelimit.test.js dist-tests/tests/sequencer.test.js dist-tests/tests/config-loader.test.js dist-tests/tests/gsm7.test.js dist-tests/tests/batch-operation.test.js"
80
80
  },
81
81
  "keywords": [
82
82
  "mcp",