skedyul 1.7.8 → 1.7.11

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/.build-stamp CHANGED
@@ -1 +1 @@
1
- 1785985393398
1
+ 1786058725534
package/dist/cli/index.js CHANGED
@@ -4658,6 +4658,39 @@ async function handleMcpBatchRoute(req, ctx) {
4658
4658
  }
4659
4659
  };
4660
4660
  }
4661
+ function isBatchOperationFailure(result) {
4662
+ return result != null && typeof result === "object" && result.success === false && typeof result.error === "object" && result.error != null;
4663
+ }
4664
+ function toStringEnv(env) {
4665
+ const out = {};
4666
+ for (const [key, value] of Object.entries(env)) {
4667
+ if (value !== void 0) {
4668
+ out[key] = value;
4669
+ }
4670
+ }
4671
+ return out;
4672
+ }
4673
+ function stripBillingFields(result) {
4674
+ const {
4675
+ billing: _billing,
4676
+ success: _success,
4677
+ error: _error,
4678
+ retry: _retry,
4679
+ ...domain
4680
+ } = result;
4681
+ return domain;
4682
+ }
4683
+ function softFailureResponse(error, retry, billing) {
4684
+ return {
4685
+ status: 200,
4686
+ body: {
4687
+ success: false,
4688
+ error,
4689
+ ...retry ? { retry } : {},
4690
+ billing: normalizeBilling(billing)
4691
+ }
4692
+ };
4693
+ }
4661
4694
  async function handleBatchOperationRoute(req, ctx) {
4662
4695
  const bodyResult = parseJsonBody(req);
4663
4696
  if (!bodyResult.success) {
@@ -4666,49 +4699,80 @@ async function handleBatchOperationRoute(req, ctx) {
4666
4699
  const body = bodyResult.data;
4667
4700
  const registry = ctx.batchOperationRegistry;
4668
4701
  if (!registry) {
4669
- return {
4670
- status: 404,
4671
- body: {
4672
- success: false,
4673
- error: "No batch operations registered"
4674
- }
4675
- };
4702
+ return softFailureResponse({
4703
+ code: "NOT_FOUND",
4704
+ message: "No batch operations registered",
4705
+ category: "validation"
4706
+ });
4676
4707
  }
4677
4708
  const operation = registry[body.handle];
4678
4709
  if (!operation) {
4679
- return {
4680
- status: 404,
4681
- body: {
4682
- success: false,
4683
- error: `Batch operation not found: ${body.handle}`
4684
- }
4685
- };
4710
+ return softFailureResponse({
4711
+ code: "NOT_FOUND",
4712
+ message: `Batch operation not found: ${body.handle}`,
4713
+ category: "validation"
4714
+ });
4686
4715
  }
4687
4716
  const context = body.context;
4688
4717
  if (!context?.workplaceId || !context?.appInstallationId || !context?.appId) {
4689
- return {
4690
- status: 400,
4691
- body: {
4692
- success: false,
4693
- error: "Missing context (workplaceId, appInstallationId, and appId required)"
4694
- }
4695
- };
4718
+ return softFailureResponse({
4719
+ code: "VALIDATION_ERROR",
4720
+ message: "Missing context (workplaceId, appInstallationId, and appId required)",
4721
+ category: "validation"
4722
+ });
4696
4723
  }
4697
- const env = body.env ?? {};
4698
- const log = {
4699
- info: (message, meta) => {
4700
- console.log(`[batch:${body.handle}] ${message}`, meta ?? "");
4701
- },
4702
- warn: (message, meta) => {
4703
- console.warn(`[batch:${body.handle}] ${message}`, meta ?? "");
4704
- },
4705
- error: (message, meta) => {
4706
- console.error(`[batch:${body.handle}] ${message}`, meta ?? "");
4707
- }
4724
+ if (body.method !== "setup" && body.method !== "iterate") {
4725
+ return softFailureResponse({
4726
+ code: "VALIDATION_ERROR",
4727
+ message: `Unknown method: ${body.method}`,
4728
+ category: "validation"
4729
+ });
4730
+ }
4731
+ const requestEnv = body.env ?? {};
4732
+ const mergedEnv = toStringEnv(buildToolExecutionEnv(requestEnv));
4733
+ const originalEnv = { ...process.env };
4734
+ Object.assign(process.env, mergedEnv);
4735
+ const log = createContextLogger();
4736
+ const app = context.app ?? {
4737
+ id: context.appId,
4738
+ versionId: context.appId
4739
+ };
4740
+ const workplace = context.workplace ?? {
4741
+ id: context.workplaceId,
4742
+ subdomain: ""
4743
+ };
4744
+ const invocation = body.invocation ?? {
4745
+ invocationId: `${body.handle}-${body.method}-${Date.now()}`,
4746
+ invocationType: "batch_operation",
4747
+ batchOperationHandle: body.handle,
4748
+ batchOperationMethod: body.method,
4749
+ appInstallationId: context.appInstallationId
4708
4750
  };
4709
4751
  const requestConfig = {
4710
- baseUrl: env.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
4711
- apiToken: env.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
4752
+ baseUrl: mergedEnv.SKEDYUL_API_URL ?? process.env.SKEDYUL_API_URL ?? "",
4753
+ apiToken: mergedEnv.SKEDYUL_API_TOKEN ?? process.env.SKEDYUL_API_TOKEN ?? ""
4754
+ };
4755
+ const rateLimitContext = {
4756
+ app,
4757
+ appInstallationId: context.appInstallationId,
4758
+ invocation,
4759
+ preAcquiredLeases: parsePreAcquiredLeases(
4760
+ mergedEnv.SKEDYUL_RATE_LIMIT_LEASES
4761
+ ),
4762
+ heldMutexQueueKeys: parseHeldMutexQueueKeys(
4763
+ mergedEnv.SKEDYUL_HELD_MUTEX_QUEUE_KEYS
4764
+ )
4765
+ };
4766
+ const opContextBase = {
4767
+ workplaceId: context.workplaceId,
4768
+ appInstallationId: context.appInstallationId,
4769
+ appId: context.appId,
4770
+ app,
4771
+ workplace,
4772
+ input: body.input,
4773
+ env: mergedEnv,
4774
+ invocation,
4775
+ log
4712
4776
  };
4713
4777
  try {
4714
4778
  if (body.method === "setup") {
@@ -4717,67 +4781,98 @@ async function handleBatchOperationRoute(req, ctx) {
4717
4781
  status: 200,
4718
4782
  body: {
4719
4783
  success: true,
4720
- result: { state: {}, total: void 0 }
4784
+ result: {
4785
+ state: {},
4786
+ total: void 0,
4787
+ cascade: operation.cascade
4788
+ },
4789
+ billing: normalizeBilling(void 0)
4721
4790
  }
4722
4791
  };
4723
4792
  }
4724
- const result = await runWithConfig(requestConfig, async () => {
4725
- return operation.setup({
4726
- workplaceId: context.workplaceId,
4727
- appInstallationId: context.appInstallationId,
4728
- appId: context.appId,
4729
- input: body.input,
4730
- env,
4731
- log
4793
+ const raw2 = await runWithConfig(requestConfig, async () => {
4794
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
4795
+ return runWithLogContext({ invocation }, async () => {
4796
+ return operation.setup(opContextBase);
4797
+ });
4732
4798
  });
4733
4799
  });
4800
+ if (isBatchOperationFailure(raw2)) {
4801
+ return softFailureResponse(raw2.error, raw2.retry, raw2.billing);
4802
+ }
4803
+ const setupResult = raw2;
4804
+ const domain2 = stripBillingFields(
4805
+ setupResult
4806
+ );
4734
4807
  return {
4735
4808
  status: 200,
4736
4809
  body: {
4737
4810
  success: true,
4738
- result
4811
+ result: {
4812
+ ...domain2,
4813
+ // Expose definition cascade to the platform workflow
4814
+ cascade: operation.cascade ?? domain2.cascade
4815
+ },
4816
+ billing: normalizeBilling(setupResult.billing)
4739
4817
  }
4740
4818
  };
4741
4819
  }
4742
- if (body.method === "iterate") {
4743
- const result = await runWithConfig(requestConfig, async () => {
4744
- return operation.iterate({
4745
- workplaceId: context.workplaceId,
4746
- appInstallationId: context.appInstallationId,
4747
- appId: context.appId,
4748
- input: body.input,
4749
- state: body.state,
4750
- page: body.page,
4751
- cursor: body.cursor,
4752
- limit: body.limit,
4753
- env,
4754
- log
4820
+ const iterateBody = body;
4821
+ const raw = await runWithConfig(requestConfig, async () => {
4822
+ return runWithRateLimitExecutionContext(rateLimitContext, async () => {
4823
+ return runWithLogContext({ invocation }, async () => {
4824
+ return operation.iterate({
4825
+ ...opContextBase,
4826
+ state: iterateBody.state,
4827
+ page: iterateBody.page,
4828
+ cursor: iterateBody.cursor,
4829
+ limit: iterateBody.limit,
4830
+ cascadeEntities: iterateBody.cascadeEntities
4831
+ });
4755
4832
  });
4756
4833
  });
4757
- return {
4758
- status: 200,
4759
- body: {
4760
- success: true,
4761
- result
4762
- }
4763
- };
4834
+ });
4835
+ if (isBatchOperationFailure(raw)) {
4836
+ return softFailureResponse(raw.error, raw.retry, raw.billing);
4764
4837
  }
4838
+ const iterateResult = raw;
4839
+ const domain = stripBillingFields(
4840
+ iterateResult
4841
+ );
4765
4842
  return {
4766
- status: 400,
4843
+ status: 200,
4767
4844
  body: {
4768
- success: false,
4769
- error: `Unknown method: ${body.method}`
4845
+ success: true,
4846
+ result: domain,
4847
+ billing: normalizeBilling(iterateResult.billing)
4770
4848
  }
4771
4849
  };
4772
4850
  } catch (err) {
4773
- log.error("Batch operation failed", { error: err });
4851
+ if (err instanceof RateLimitExceededError) {
4852
+ return softFailureResponse(
4853
+ {
4854
+ code: "RATE_LIMITED",
4855
+ message: err.message,
4856
+ category: "external"
4857
+ },
4858
+ { allowed: true, afterMs: err.retryAfterMs }
4859
+ );
4860
+ }
4861
+ log.error("Batch operation failed", err);
4774
4862
  return {
4775
4863
  status: 500,
4776
4864
  body: {
4777
4865
  success: false,
4778
- error: err instanceof Error ? err.message : String(err)
4866
+ error: {
4867
+ code: "INTERNAL_ERROR",
4868
+ message: err instanceof Error ? err.message : String(err),
4869
+ category: "internal"
4870
+ },
4871
+ billing: normalizeBilling(void 0)
4779
4872
  }
4780
4873
  };
4874
+ } finally {
4875
+ process.env = originalEnv;
4781
4876
  }
4782
4877
  }
4783
4878
  function createNotFoundResponse() {
@@ -5,6 +5,10 @@
5
5
  * like member imports, data syncs, and batch API calls. The platform handles
6
6
  * orchestration, progress tracking, CRM mapping, and upsert.
7
7
  */
8
+ import type { InvocationContext } from '../../types/invocation';
9
+ import type { ToolBilling, ToolError, ToolRetry } from '../../types/tool';
10
+ import type { AppInfo, WorkplaceInfo } from '../../types/shared';
11
+ export type { ToolBilling, ToolError, ToolRetry };
8
12
  /**
9
13
  * Pagination response from an iterate function.
10
14
  * Supports both cursor-based and page-based pagination.
@@ -21,6 +25,15 @@ export interface BatchPagination {
21
25
  /** Items per page */
22
26
  limit?: number;
23
27
  }
28
+ /**
29
+ * Soft-failure return from setup/iterate (tool-call shaped).
30
+ */
31
+ export interface BatchOperationFailure {
32
+ success: false;
33
+ error: ToolError;
34
+ retry?: ToolRetry;
35
+ billing?: ToolBilling;
36
+ }
24
37
  /**
25
38
  * Result from a batch operation setup function.
26
39
  */
@@ -29,18 +42,59 @@ export interface BatchOperationSetupResult {
29
42
  state?: Record<string, unknown>;
30
43
  /** Optional total count for progress display */
31
44
  total?: number;
45
+ /**
46
+ * Cascade phases (usually injected from the operation definition by the
47
+ * route handler so the platform workflow can resolve map-gated targets).
48
+ */
49
+ cascade?: BatchCascadePhase[];
50
+ /** Optional billing (normalized by the route if omitted) */
51
+ billing?: ToolBilling;
52
+ /** Optional success discriminator (default true when domain fields present) */
53
+ success?: true;
54
+ }
55
+ /**
56
+ * Cascade phase for multi-entity batch imports.
57
+ * Platform upserts configured entities in `order` within each wave.
58
+ */
59
+ export interface BatchCascadePhase {
60
+ /** App entity handle to upsert */
61
+ entity: string;
62
+ /** Lower runs first within the same wave */
63
+ order: number;
64
+ /** setup = once (typically first iterate); page = every iterate page */
65
+ wave: 'setup' | 'page';
66
+ /**
67
+ * Other cascade entities that must also be CRM-configured for this phase
68
+ * to run (e.g. plan requires package).
69
+ */
70
+ requires?: string[];
32
71
  }
33
72
  /**
34
73
  * Result from a batch operation iterate function.
35
74
  */
36
75
  export interface BatchOperationIterateResult {
37
- /** Array of raw items from the external source */
76
+ /**
77
+ * Array of raw items for the primary `entity`.
78
+ * Prefer `itemsByEntity` for cascade imports; when both are set, platform
79
+ * uses `itemsByEntity` and falls back to `items` for the primary entity.
80
+ */
38
81
  items: Record<string, unknown>[];
82
+ /**
83
+ * Multi-entity items keyed by entity handle (cascade imports).
84
+ * Platform upserts each configured entity in cascade order.
85
+ */
86
+ itemsByEntity?: Record<string, Record<string, unknown>[]>;
39
87
  /** Pagination info for fetching more pages */
40
88
  pagination: BatchPagination;
41
89
  /** Optional state to persist for the next iterate call */
42
90
  state?: Record<string, unknown>;
91
+ /** Optional billing (normalized by the route if omitted) */
92
+ billing?: ToolBilling;
93
+ /** Optional success discriminator (default true when domain fields present) */
94
+ success?: true;
43
95
  }
96
+ export type BatchOperationSetupReturn = BatchOperationSetupResult | BatchOperationFailure;
97
+ export type BatchOperationIterateReturn = BatchOperationIterateResult | BatchOperationFailure;
44
98
  /**
45
99
  * Context passed to batch operation setup and iterate functions.
46
100
  */
@@ -51,15 +105,27 @@ export interface BatchOperationContext {
51
105
  appInstallationId: string;
52
106
  /** App ID */
53
107
  appId: string;
108
+ /** App info with version (for rate-limit / SDK context) */
109
+ app?: AppInfo;
110
+ /** Workplace info when available */
111
+ workplace?: WorkplaceInfo;
54
112
  /** User-provided input (if any) */
55
113
  input?: Record<string, unknown>;
56
114
  /** State from previous setup or iterate call */
57
115
  state?: Record<string, unknown>;
58
116
  /**
59
- * Runtime environment variables from the platform invoke
60
- * (APP_INSTALL overrides + SKEDYUL_API_TOKEN / SKEDYUL_API_URL).
117
+ * Merged environment (process.env + baked MCP_ENV + request env),
118
+ * same layering as tool handlers via buildToolExecutionEnv.
61
119
  */
62
120
  env: Record<string, string>;
121
+ /** Invocation context for log traceability */
122
+ invocation?: InvocationContext;
123
+ /**
124
+ * Entity handles whose CRM maps are configured for this job's cascade.
125
+ * Platform injects this on iterate so apps can skip expensive fetches
126
+ * (e.g. per-member credits) when that entity is not mapped.
127
+ */
128
+ cascadeEntities?: string[];
63
129
  /** Logger instance */
64
130
  log: {
65
131
  info: (message: string, meta?: Record<string, unknown>) => void;
@@ -70,7 +136,7 @@ export interface BatchOperationContext {
70
136
  /**
71
137
  * Type for batch operation setup function.
72
138
  */
73
- export type BatchOperationSetupFn = (ctx: BatchOperationContext) => Promise<BatchOperationSetupResult>;
139
+ export type BatchOperationSetupFn = (ctx: BatchOperationContext) => Promise<BatchOperationSetupReturn>;
74
140
  /**
75
141
  * Type for batch operation iterate function.
76
142
  */
@@ -81,7 +147,7 @@ export type BatchOperationIterateFn = (ctx: BatchOperationContext & {
81
147
  cursor?: string | number;
82
148
  /** Items per page */
83
149
  limit: number;
84
- }) => Promise<BatchOperationIterateResult>;
150
+ }) => Promise<BatchOperationIterateReturn>;
85
151
  /**
86
152
  * Batch operation definition.
87
153
  *
@@ -121,10 +187,15 @@ export interface BatchOperationDefinition {
121
187
  /** Description of what this operation does */
122
188
  description?: string;
123
189
  /**
124
- * Entity handle that items will be upserted to.
190
+ * Primary entity handle (progress + start gate).
125
191
  * The platform applies CRM mappings and calls upsertMany.
126
192
  */
127
193
  entity: string;
194
+ /**
195
+ * Optional multi-entity cascade. When set, iterate may return
196
+ * `itemsByEntity` and the platform upserts map-configured phases in order.
197
+ */
198
+ cascade?: BatchCascadePhase[];
128
199
  /**
129
200
  * Setup function called once at the start of the operation.
130
201
  * Use to initialize state, fetch total count, etc.