@ycadence/sdk 0.1.0 → 0.3.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/src/errors.ts ADDED
@@ -0,0 +1,144 @@
1
+ /**
2
+ * Errors the client throws.
3
+ *
4
+ * One base class, `YCadenceError`, so `instanceof YCadenceError` catches every
5
+ * failure the client produces, and a subclass per kind of failure, so a caller
6
+ * can branch on the class instead of comparing status codes. The base keeps its
7
+ * original positional constructor `(status, code, message, issues)`, because
8
+ * integrations construct it in their own tests.
9
+ */
10
+
11
+ export interface ApiIssue {
12
+ path: string
13
+ message: string
14
+ }
15
+
16
+ /** The error body every v1 route returns (`packages/api/src/types.ts`). */
17
+ export interface ApiFailure {
18
+ error: string
19
+ code: string
20
+ issues?: ApiIssue[]
21
+ /** Added by the work management contracts; absent on older servers. */
22
+ details?: Record<string, unknown>
23
+ requestId?: string
24
+ }
25
+
26
+ export interface ErrorExtras {
27
+ /** The server's request id, from the body or the `x-request-id` header. */
28
+ requestId?: string
29
+ details?: Record<string, unknown>
30
+ /** Seconds, from `Retry-After`, when the server sent one. */
31
+ retryAfter?: number
32
+ method?: string
33
+ path?: string
34
+ }
35
+
36
+ export class YCadenceError extends Error {
37
+ readonly requestId: string | undefined
38
+ readonly details: Record<string, unknown> | undefined
39
+ readonly retryAfter: number | undefined
40
+ readonly method: string | undefined
41
+ readonly path: string | undefined
42
+
43
+ constructor(
44
+ readonly status: number,
45
+ readonly code: string,
46
+ message: string,
47
+ readonly issues?: ApiIssue[],
48
+ extras: ErrorExtras = {}
49
+ ) {
50
+ super(message)
51
+ this.name = 'YCadenceError'
52
+ this.requestId = extras.requestId
53
+ this.details = extras.details
54
+ this.retryAfter = extras.retryAfter
55
+ this.method = extras.method
56
+ this.path = extras.path
57
+ }
58
+
59
+ /**
60
+ * Whether retrying unchanged could plausibly succeed: rate limits, server
61
+ * errors, timeouts and an unreachable host. A 4xx means the request itself
62
+ * needs to change.
63
+ */
64
+ get retryable(): boolean {
65
+ return this.status === 0 || this.status === 408 || this.status === 429 || this.status >= 500
66
+ }
67
+ }
68
+
69
+ /** 400 or 422: the request was understood and refused as invalid. */
70
+ export class YCadenceValidationError extends YCadenceError {
71
+ override name = 'YCadenceValidationError'
72
+ }
73
+
74
+ /** 401 or 403: no credential, a bad one, or not allowed. */
75
+ export class YCadencePermissionError extends YCadenceError {
76
+ override name = 'YCadencePermissionError'
77
+ }
78
+
79
+ /** 404: missing, deleted, or in another organization. */
80
+ export class YCadenceNotFoundError extends YCadenceError {
81
+ override name = 'YCadenceNotFoundError'
82
+ }
83
+
84
+ /** 409: a domain rule, a version conflict, or the same idempotency key still running. */
85
+ export class YCadenceConflictError extends YCadenceError {
86
+ override name = 'YCadenceConflictError'
87
+ }
88
+
89
+ /** 412: an `If-Match` precondition failed; reload and try again. */
90
+ export class YCadencePreconditionError extends YCadenceError {
91
+ override name = 'YCadencePreconditionError'
92
+ }
93
+
94
+ /** 429: slow down; `retryAfter` says for how long when the server said. */
95
+ export class YCadenceRateLimitError extends YCadenceError {
96
+ override name = 'YCadenceRateLimitError'
97
+ }
98
+
99
+ /** Status 0 or 408: the host could not be reached, or did not answer in time. */
100
+ export class YCadenceNetworkError extends YCadenceError {
101
+ override name = 'YCadenceNetworkError'
102
+ }
103
+
104
+ /** The class that matches a status. */
105
+ export function errorClassFor(status: number): typeof YCadenceError {
106
+ if (status === 0 || status === 408) return YCadenceNetworkError
107
+ if (status === 400 || status === 422) return YCadenceValidationError
108
+ if (status === 401 || status === 403) return YCadencePermissionError
109
+ if (status === 404) return YCadenceNotFoundError
110
+ if (status === 409) return YCadenceConflictError
111
+ if (status === 412) return YCadencePreconditionError
112
+ if (status === 429) return YCadenceRateLimitError
113
+ return YCadenceError
114
+ }
115
+
116
+ export function createError(
117
+ status: number,
118
+ code: string,
119
+ message: string,
120
+ issues?: ApiIssue[],
121
+ extras: ErrorExtras = {}
122
+ ): YCadenceError {
123
+ const ErrorClass = errorClassFor(status)
124
+ return new ErrorClass(status, code, message, issues, extras)
125
+ }
126
+
127
+ /**
128
+ * Thrown, unchanged, through the client by a custom `fetch` that wants to stop
129
+ * a request without it being reported as a network error. The CLI's
130
+ * `--dry-run` uses it to show a mutation instead of sending it.
131
+ */
132
+ export class RequestIntercepted extends Error {
133
+ override name = 'RequestIntercepted'
134
+ constructor(
135
+ readonly request: {
136
+ method: string
137
+ url: string
138
+ headers: Record<string, string>
139
+ body: unknown
140
+ }
141
+ ) {
142
+ super(`${request.method} ${request.url} was not sent.`)
143
+ }
144
+ }
package/src/index.ts CHANGED
@@ -1,10 +1,37 @@
1
1
  export {
2
- type ApiFailure,
3
2
  type ClientOptions,
4
3
  DevPulseClient,
5
4
  DevPulseError,
6
- type Page,
7
- type WorkItem,
5
+ parseRetryAfter,
6
+ type RequestOptions,
7
+ type ResponseInfo,
8
+ type RetryOptions,
8
9
  YCadenceClient,
9
- YCadenceError,
10
10
  } from './client.js'
11
+ export {
12
+ type ApiFailure,
13
+ type ApiIssue,
14
+ createError,
15
+ type ErrorExtras,
16
+ errorClassFor,
17
+ RequestIntercepted,
18
+ YCadenceConflictError,
19
+ YCadenceError,
20
+ YCadenceNetworkError,
21
+ YCadenceNotFoundError,
22
+ YCadencePermissionError,
23
+ YCadencePreconditionError,
24
+ YCadenceRateLimitError,
25
+ YCadenceValidationError,
26
+ } from './errors.js'
27
+ export {
28
+ buildPath,
29
+ type HttpMethod,
30
+ isIdempotentMethod,
31
+ OPERATIONS,
32
+ type Operation,
33
+ type OperationId,
34
+ } from './operations.js'
35
+ export { collect, paginate } from './pagination.js'
36
+ export * from './types.js'
37
+ export { SDK_VERSION } from './version.js'
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Every API operation the client calls, in one table.
3
+ *
4
+ * The client builds each request from this table rather than from a path
5
+ * written inline in a method, so there is one place to compare with the
6
+ * server: `apps/web/lib/api/sdk-contract.test.ts` fails when an entry has no
7
+ * matching route contract (unless it is marked `pending`), and the CLI's
8
+ * reference and `--dry-run` name operations by these ids.
9
+ */
10
+
11
+ export type HttpMethod = 'GET' | 'POST' | 'PATCH' | 'PUT' | 'DELETE'
12
+
13
+ export interface Operation {
14
+ method: HttpMethod
15
+ /** The route contract's path, with `{param}` placeholders. */
16
+ path: string
17
+ /**
18
+ * Planned by the work management contracts and not served yet. The client
19
+ * still calls it; a 404 from an older server is reported as such.
20
+ */
21
+ pending?: string
22
+ }
23
+
24
+ export const OPERATIONS = {
25
+ health: { method: 'GET', path: '/api/health' },
26
+ me: { method: 'GET', path: '/api/v1/me' },
27
+ listProjects: { method: 'GET', path: '/api/v1/projects' },
28
+
29
+ listWorkItems: { method: 'GET', path: '/api/v1/projects/{projectId}/work-items' },
30
+ createWorkItem: { method: 'POST', path: '/api/v1/projects/{projectId}/work-items' },
31
+ getWorkItem: { method: 'GET', path: '/api/v1/work-items/{workItemId}' },
32
+ updateWorkItem: { method: 'PATCH', path: '/api/v1/work-items/{workItemId}' },
33
+ deleteWorkItem: { method: 'DELETE', path: '/api/v1/work-items/{workItemId}' },
34
+
35
+ listStates: { method: 'GET', path: '/api/v1/projects/{projectId}/states' },
36
+ listLabels: { method: 'GET', path: '/api/v1/projects/{projectId}/labels' },
37
+ createLabel: { method: 'POST', path: '/api/v1/projects/{projectId}/labels' },
38
+ listTypes: { method: 'GET', path: '/api/v1/projects/{projectId}/types' },
39
+
40
+ listCycles: { method: 'GET', path: '/api/v1/projects/{projectId}/cycles' },
41
+ createCycle: { method: 'POST', path: '/api/v1/projects/{projectId}/cycles' },
42
+ getCycleProgress: { method: 'GET', path: '/api/v1/cycles/{cycleId}/progress' },
43
+ addWorkItemsToCycle: { method: 'POST', path: '/api/v1/cycles/{cycleId}/work-items' },
44
+
45
+ listViews: { method: 'GET', path: '/api/v1/projects/{projectId}/views' },
46
+ createView: { method: 'POST', path: '/api/v1/projects/{projectId}/views' },
47
+
48
+ listIntake: { method: 'GET', path: '/api/v1/projects/{projectId}/intake' },
49
+ submitIntake: { method: 'POST', path: '/api/v1/projects/{projectId}/intake' },
50
+ triageIntake: { method: 'POST', path: '/api/v1/intake-items/{intakeItemId}/triage' },
51
+
52
+ previewImport: { method: 'POST', path: '/api/v1/projects/{projectId}/imports/preview' },
53
+ startImport: { method: 'POST', path: '/api/v1/projects/{projectId}/imports' },
54
+ listImports: { method: 'GET', path: '/api/v1/projects/{projectId}/imports' },
55
+ getImport: { method: 'GET', path: '/api/v1/imports/{runId}' },
56
+ } as const satisfies Record<string, Operation>
57
+
58
+ export type OperationId = keyof typeof OPERATIONS
59
+
60
+ /** GET and HEAD are safe to repeat; anything else needs an idempotency key first. */
61
+ export function isIdempotentMethod(method: string): boolean {
62
+ return method === 'GET' || method === 'HEAD'
63
+ }
64
+
65
+ /** Fill `{param}` placeholders, encoding each value. */
66
+ export function buildPath(template: string, params: Record<string, string> = {}): string {
67
+ return template.replace(/\{(\w+)\}/g, (_match, name: string) => {
68
+ const value = params[name]
69
+ if (value === undefined || value === '') {
70
+ throw new TypeError(`Missing path parameter "${name}" for ${template}.`)
71
+ }
72
+ return encodeURIComponent(value)
73
+ })
74
+ }
@@ -0,0 +1,36 @@
1
+ import type { Page } from './types'
2
+
3
+ /**
4
+ * Follow `nextCursor` to the end, one page at a time.
5
+ *
6
+ * Lazy: a caller that breaks out early pays for the pages it read and no more.
7
+ */
8
+ export async function* paginate<T>(
9
+ fetchPage: (cursor: string | undefined) => Promise<Page<T>>,
10
+ options: { cursor?: string; maxItems?: number } = {}
11
+ ): AsyncGenerator<T> {
12
+ let cursor = options.cursor
13
+ let yielded = 0
14
+ do {
15
+ const page = await fetchPage(cursor)
16
+ for (const item of page.items) {
17
+ if (options.maxItems !== undefined && yielded >= options.maxItems) return
18
+ yielded += 1
19
+ yield item
20
+ }
21
+ cursor = page.nextCursor ?? undefined
22
+ } while (cursor !== undefined)
23
+ }
24
+
25
+ /** Every item from {@link paginate}, as an array, up to `maxItems`. */
26
+ export async function collect<T>(
27
+ iterable: AsyncIterable<T>,
28
+ maxItems = Number.POSITIVE_INFINITY
29
+ ): Promise<T[]> {
30
+ const items: T[] = []
31
+ for await (const item of iterable) {
32
+ if (items.length >= maxItems) break
33
+ items.push(item)
34
+ }
35
+ return items
36
+ }
package/src/types.ts ADDED
@@ -0,0 +1,368 @@
1
+ /**
2
+ * Request and response shapes of the v1 API, as they arrive over JSON.
3
+ *
4
+ * The routes define these with zod in `apps/web` (`lib/api/contracts.ts` and
5
+ * the route files). The SDK is published without dependencies, so it cannot
6
+ * import those schemas; instead each response type has a field list below that
7
+ * the compiler checks against the type, and `apps/web/lib/api/sdk-contract.test.ts`
8
+ * checks against the route's zod schema. A field added on one side and not the
9
+ * other fails that test rather than reaching a user.
10
+ *
11
+ * Dates are ISO strings: that is what JSON carries.
12
+ */
13
+
14
+ export type Priority = 'urgent' | 'high' | 'medium' | 'low' | 'none'
15
+ export const PRIORITIES: readonly Priority[] = ['urgent', 'high', 'medium', 'low', 'none']
16
+
17
+ export type StateGroup = 'backlog' | 'unstarted' | 'started' | 'completed' | 'cancelled' | 'triage'
18
+
19
+ export interface WorkItem {
20
+ id: string
21
+ key: string
22
+ sequenceId: number
23
+ name: string
24
+ priority: Priority
25
+ stateId: string | null
26
+ assigneeIds: string[]
27
+ labelIds: string[]
28
+ parentId: string | null
29
+ sortOrder: string
30
+ createdAt: string
31
+ /**
32
+ * Returned by `GET /work-items/{id}` (always, from Slice 2A on); not yet by
33
+ * lists or writes.
34
+ */
35
+ projectId?: string
36
+ /** Not returned yet. Sent back as `If-Match` once the server versions items. */
37
+ version?: number
38
+ }
39
+
40
+ export interface WorkItemDetail extends WorkItem {
41
+ /** Optional only so the SDK still reads a server older than Slice 2A. */
42
+ projectId?: string
43
+ description: string | null
44
+ startDate: string | null
45
+ targetDate: string | null
46
+ archivedAt: string | null
47
+ updatedAt: string
48
+ }
49
+
50
+ export interface Page<T> {
51
+ items: T[]
52
+ nextCursor: string | null
53
+ }
54
+
55
+ export interface CreateWorkItemInput {
56
+ name: string
57
+ description?: string
58
+ stateId?: string
59
+ priority?: Priority
60
+ assigneeIds?: string[]
61
+ labelIds?: string[]
62
+ parentId?: string
63
+ /** ISO date. */
64
+ startDate?: string
65
+ targetDate?: string
66
+ /** One of the project's work item types; absent means the project's default. */
67
+ typeId?: string
68
+ }
69
+
70
+ /**
71
+ * The fields PATCH accepts today. The server strips anything else, so sending
72
+ * an unlisted field would look like success and change nothing.
73
+ */
74
+ export interface UpdateWorkItemInput {
75
+ name?: string
76
+ description?: string
77
+ stateId?: string
78
+ priority?: Priority
79
+ parentId?: string | null
80
+ startDate?: string | null
81
+ targetDate?: string | null
82
+ /** A work item type of the item's project; null clears it. */
83
+ typeId?: string | null
84
+ }
85
+
86
+ export interface ListWorkItemsOptions {
87
+ /** A @devpulse/query expression; sent JSON-encoded as `filter`. */
88
+ filter?: unknown
89
+ limit?: number
90
+ cursor?: string
91
+ /** A state id. */
92
+ state?: string
93
+ priority?: Priority
94
+ }
95
+
96
+ export interface State {
97
+ id: string
98
+ name: string
99
+ group: StateGroup | string
100
+ color: string
101
+ description: string | null
102
+ /** Fractional-index sort key; the list arrives already ordered by it. */
103
+ sequence: string
104
+ isDefault: boolean
105
+ }
106
+
107
+ export interface Label {
108
+ id: string
109
+ name: string
110
+ color: string
111
+ description: string | null
112
+ sortOrder: string
113
+ }
114
+
115
+ export interface Cycle {
116
+ id: string
117
+ name: string
118
+ description: string | null
119
+ startDate: string | null
120
+ endDate: string | null
121
+ status: 'draft' | 'upcoming' | 'active' | 'completed'
122
+ sortOrder: string
123
+ completedAt: string | null
124
+ archivedAt: string | null
125
+ }
126
+
127
+ export interface CycleProgress {
128
+ progress: unknown
129
+ burndown: unknown
130
+ frozen: boolean
131
+ }
132
+
133
+ export interface View {
134
+ id: string
135
+ name: string
136
+ description: string | null
137
+ filter: unknown
138
+ display: unknown
139
+ access: 'private' | 'public'
140
+ ownerId: string | null
141
+ sortOrder: string
142
+ }
143
+
144
+ export interface CreateViewInput {
145
+ name: string
146
+ description?: string
147
+ where?: unknown
148
+ display?: Record<string, unknown>
149
+ access?: 'private' | 'public'
150
+ }
151
+
152
+ export type IntakeStatus = 'pending' | 'accepted' | 'declined' | 'snoozed' | 'duplicate'
153
+
154
+ export interface IntakeItem {
155
+ id: string
156
+ workItemId: string
157
+ status: IntakeStatus
158
+ source: string
159
+ sourceEmail: string | null
160
+ snoozedUntil: string | null
161
+ submittedAt: string
162
+ }
163
+
164
+ export interface SubmitIntakeInput {
165
+ title: string
166
+ description?: string
167
+ source?: 'in_app' | 'form' | 'email' | 'api' | 'integration'
168
+ sourceEmail?: string
169
+ extra?: Record<string, unknown>
170
+ }
171
+
172
+ export interface SubmitIntakeResult {
173
+ intakeItemId: string
174
+ workItemId: string
175
+ sequenceId: number
176
+ }
177
+
178
+ export interface TriageInput {
179
+ status: 'accepted' | 'declined' | 'snoozed' | 'duplicate'
180
+ /** ISO date; required when snoozing. */
181
+ snoozedUntil?: string
182
+ /** Required when marking a duplicate. */
183
+ duplicateOfId?: string
184
+ }
185
+
186
+ export interface TriageResult {
187
+ status: string
188
+ movedToStateId: string | null
189
+ }
190
+
191
+ export interface ImportPreview {
192
+ source: string
193
+ total: number
194
+ states: { name: string; suggestedStateId: string | null }[]
195
+ labels: { name: string; exists: boolean }[]
196
+ assignees: { value: string; matched: boolean }[]
197
+ skipped: { row: number; reason: string }[]
198
+ unmappedPriorities: string[]
199
+ /**
200
+ * False when re-importing would create duplicates rather than update — a CSV
201
+ * with no id column. Worth showing before the import, not after.
202
+ */
203
+ idempotent: boolean
204
+ }
205
+
206
+ export interface ImportRun {
207
+ id: string
208
+ source: string
209
+ status: string
210
+ totalItems: number
211
+ createdCount: number
212
+ updatedCount: number
213
+ cursor: number
214
+ skipped: { row: number; reason: string }[]
215
+ unmatched: { kind: string; value: string; item: string }[]
216
+ error: string | null
217
+ startedAt?: string | null
218
+ finishedAt?: string | null
219
+ createdAt?: string
220
+ }
221
+
222
+ export type ImportSource = 'csv' | 'github' | 'jira' | 'linear'
223
+
224
+ export interface ImportInput {
225
+ source: ImportSource
226
+ /** CSV text, or the parsed JSON export for the API sources. */
227
+ data: unknown
228
+ repository?: string
229
+ baseUrl?: string
230
+ /** Source state name to state id. Only for `startImport`. */
231
+ stateMap?: Record<string, string>
232
+ }
233
+
234
+ export interface Health {
235
+ status: string
236
+ checks: Record<string, string>
237
+ }
238
+
239
+ /** `GET /api/v1/me`: who the credential is. Never includes the key. */
240
+ export interface Me {
241
+ principal: {
242
+ kind: 'personal_key' | 'service_key' | string
243
+ scopeType?: 'project' | 'organization' | 'personal' | string
244
+ apiKeyId: string
245
+ keyPrefix: string
246
+ userId: string | null
247
+ email: string | null
248
+ organizationRole?: string | null
249
+ }
250
+ organization: { id: string; slug: string; name: string }
251
+ project: { id: string; identifier: string; name: string } | null
252
+ scopes: string[]
253
+ expiresAt: string | null
254
+ }
255
+
256
+ /** `GET /api/v1/projects`. */
257
+ export interface Project {
258
+ id: string
259
+ identifier: string
260
+ name: string
261
+ archivedAt: string | null
262
+ }
263
+
264
+ /**
265
+ * Field lists, checked by the compiler to name every key of the type (and no
266
+ * others), and by the contract test to match the route's response schema.
267
+ */
268
+ type FieldsOf<T> = { [K in keyof Required<T>]: true }
269
+
270
+ export const WORK_ITEM_FIELDS: FieldsOf<WorkItem> = {
271
+ id: true,
272
+ key: true,
273
+ sequenceId: true,
274
+ name: true,
275
+ priority: true,
276
+ stateId: true,
277
+ assigneeIds: true,
278
+ labelIds: true,
279
+ parentId: true,
280
+ sortOrder: true,
281
+ createdAt: true,
282
+ projectId: true,
283
+ version: true,
284
+ }
285
+
286
+ export const WORK_ITEM_DETAIL_FIELDS: FieldsOf<WorkItemDetail> = {
287
+ ...WORK_ITEM_FIELDS,
288
+ description: true,
289
+ startDate: true,
290
+ targetDate: true,
291
+ archivedAt: true,
292
+ updatedAt: true,
293
+ }
294
+
295
+ export const STATE_FIELDS: FieldsOf<State> = {
296
+ id: true,
297
+ name: true,
298
+ group: true,
299
+ color: true,
300
+ description: true,
301
+ sequence: true,
302
+ isDefault: true,
303
+ }
304
+
305
+ export const LABEL_FIELDS: FieldsOf<Label> = {
306
+ id: true,
307
+ name: true,
308
+ color: true,
309
+ description: true,
310
+ sortOrder: true,
311
+ }
312
+
313
+ export const CYCLE_FIELDS: FieldsOf<Cycle> = {
314
+ id: true,
315
+ name: true,
316
+ description: true,
317
+ startDate: true,
318
+ endDate: true,
319
+ status: true,
320
+ sortOrder: true,
321
+ completedAt: true,
322
+ archivedAt: true,
323
+ }
324
+
325
+ export const VIEW_FIELDS: FieldsOf<View> = {
326
+ id: true,
327
+ name: true,
328
+ description: true,
329
+ filter: true,
330
+ display: true,
331
+ access: true,
332
+ ownerId: true,
333
+ sortOrder: true,
334
+ }
335
+
336
+ export const INTAKE_ITEM_FIELDS: FieldsOf<IntakeItem> = {
337
+ id: true,
338
+ workItemId: true,
339
+ status: true,
340
+ source: true,
341
+ sourceEmail: true,
342
+ snoozedUntil: true,
343
+ submittedAt: true,
344
+ }
345
+
346
+ export const IMPORT_RUN_FIELDS: FieldsOf<ImportRun> = {
347
+ id: true,
348
+ source: true,
349
+ status: true,
350
+ totalItems: true,
351
+ createdCount: true,
352
+ updatedCount: true,
353
+ cursor: true,
354
+ skipped: true,
355
+ unmatched: true,
356
+ error: true,
357
+ startedAt: true,
358
+ finishedAt: true,
359
+ createdAt: true,
360
+ }
361
+
362
+ /**
363
+ * Fields the SDK declares ahead of the server (the work management contracts
364
+ * add them). The contract test allows them to be missing from today's schemas.
365
+ */
366
+ export const PENDING_FIELDS: Record<string, readonly string[]> = {
367
+ workItem: ['projectId', 'version'],
368
+ }
package/src/version.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * The published version, for `User-Agent`.
3
+ *
4
+ * Replaced at build time (`bun build --define`) from package.json, so the
5
+ * Version PR that bumps package.json needs no second edit. Unbuilt source —
6
+ * tests, the workspace — reports `0.0.0-dev`.
7
+ */
8
+ declare const __YCADENCE_SDK_VERSION__: string | undefined
9
+
10
+ export const SDK_VERSION: string =
11
+ typeof __YCADENCE_SDK_VERSION__ === 'string' ? __YCADENCE_SDK_VERSION__ : '0.0.0-dev'