deepline 0.3.108 → 0.3.110

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.
@@ -200,7 +200,7 @@ export const SDK_RELEASE = {
200
200
  // getters keep their established compatibility behavior.
201
201
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
202
202
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
203
- version: '0.3.108',
203
+ version: '0.3.110',
204
204
  updateSummary:
205
205
  'Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.',
206
206
  packageCapabilities: {
@@ -903,6 +903,24 @@
903
903
  "failureAlert": "convex_action_wrapper",
904
904
  "domainReportRequired": true
905
905
  },
906
+ {
907
+ "id": "convex.process-notification-usage-refreshes",
908
+ "name": "Process product notification usage refreshes",
909
+ "scheduler": "convex_cron",
910
+ "cadence": "every 60s",
911
+ "timezone": "UTC",
912
+ "source": "packages/convex/crons.ts",
913
+ "locator": "process notification usage refreshes",
914
+ "owner": "platform",
915
+ "criticality": "critical",
916
+ "purpose": "Recover missed usage-notification wakes and coalesce latest-state observations per organization.",
917
+ "successDefinition": "Queued usage wakes are observed and retired after their notification transaction commits; an empty queue is a valid no-op.",
918
+ "failureDefinition": "The worker cannot read or commit a batch, leaving usage wakes for the next retry and reporting the failure.",
919
+ "startGraceMs": 30000,
920
+ "runTimeoutMs": 1800000,
921
+ "failureAlert": "convex_action_wrapper",
922
+ "domainReportRequired": true
923
+ },
906
924
  {
907
925
  "id": "convex.fail-stale-play-deployments",
908
926
  "name": "Fail stale play deployments",
@@ -18,6 +18,7 @@ export const MANUALLY_RUNNABLE_CONVEX_CRON_JOB_IDS = [
18
18
  'convex.monitor-deployment-job-tick',
19
19
  'convex.provider-monitor-global-audit-daily',
20
20
  'convex.recover-notification-delivery-work',
21
+ 'convex.process-notification-usage-refreshes',
21
22
  'convex.observe-operational-queue-health',
22
23
  'convex.cleanup-expired-notification-audit-records',
23
24
  'convex.fail-stale-play-deployments',
@@ -409,26 +409,26 @@ function absurdProjectionQuery(queueTable: string): string {
409
409
  FROM ${table}
410
410
  WHERE state = 'pending' AND available_at <= $1::timestamptz
411
411
  ORDER BY available_at, run_id
412
- LIMIT $3)
412
+ LIMIT $2)
413
413
  UNION ALL
414
414
  (SELECT 'delayed'::text, available_at
415
415
  FROM ${table}
416
416
  WHERE state = 'sleeping'
417
417
  ORDER BY available_at, run_id
418
- LIMIT $3)
418
+ LIMIT $2)
419
419
  UNION ALL
420
420
  (SELECT 'delayed'::text, available_at
421
421
  FROM ${table}
422
422
  WHERE state = 'pending' AND available_at > $1::timestamptz
423
423
  ORDER BY available_at, run_id
424
- LIMIT $3)
424
+ LIMIT $2)
425
425
  UNION ALL
426
426
  (SELECT 'in_flight'::text, coalesce(started_at, created_at)
427
427
  FROM ${table}
428
428
  WHERE state = 'running'
429
429
  AND (claim_expires_at IS NULL OR claim_expires_at > $1::timestamptz)
430
430
  ORDER BY available_at, run_id
431
- LIMIT $3)
431
+ LIMIT $2)
432
432
  UNION ALL
433
433
  (SELECT 'blocked'::text, coalesce(claim_expires_at, started_at, created_at)
434
434
  FROM ${table}
@@ -436,30 +436,30 @@ function absurdProjectionQuery(queueTable: string): string {
436
436
  AND claim_expires_at IS NOT NULL
437
437
  AND claim_expires_at <= $1::timestamptz
438
438
  ORDER BY claim_expires_at, run_id
439
- LIMIT $3)
439
+ LIMIT $2)
440
440
  UNION ALL
441
441
  (SELECT 'dead_lettered'::text, coalesce(failed_at, created_at)
442
442
  FROM ${table}
443
443
  WHERE state = 'failed'
444
444
  ORDER BY available_at, run_id
445
- LIMIT $3)
445
+ LIMIT $2)
446
446
  )
447
447
  SELECT
448
- least(count(*) FILTER (WHERE bucket = 'ready'), $4) AS ready,
449
- least(count(*) FILTER (WHERE bucket = 'delayed'), $4) AS delayed,
450
- least(count(*) FILTER (WHERE bucket = 'in_flight'), $4) AS in_flight,
451
- least(count(*) FILTER (WHERE bucket = 'blocked'), $4) AS blocked,
452
- least(count(*) FILTER (WHERE bucket = 'dead_lettered'), $4) AS dead_lettered,
448
+ least(count(*) FILTER (WHERE bucket = 'ready'), $3) AS ready,
449
+ least(count(*) FILTER (WHERE bucket = 'delayed'), $3) AS delayed,
450
+ least(count(*) FILTER (WHERE bucket = 'in_flight'), $3) AS in_flight,
451
+ least(count(*) FILTER (WHERE bucket = 'blocked'), $3) AS blocked,
452
+ least(count(*) FILTER (WHERE bucket = 'dead_lettered'), $3) AS dead_lettered,
453
453
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'ready') AS oldest_ready_ms,
454
454
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'delayed') AS oldest_delayed_ms,
455
455
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'in_flight') AS oldest_in_flight_ms,
456
456
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'blocked') AS oldest_blocked_ms,
457
457
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'dead_lettered') AS oldest_dead_lettered_ms,
458
- count(*) FILTER (WHERE bucket = 'ready') > $4 AS ready_truncated,
459
- count(*) FILTER (WHERE bucket = 'delayed') > $4 AS delayed_truncated,
460
- count(*) FILTER (WHERE bucket = 'in_flight') > $4 AS in_flight_truncated,
461
- count(*) FILTER (WHERE bucket = 'blocked') > $4 AS blocked_truncated,
462
- count(*) FILTER (WHERE bucket = 'dead_lettered') > $4 AS dead_lettered_truncated,
458
+ count(*) FILTER (WHERE bucket = 'ready') > $3 AS ready_truncated,
459
+ count(*) FILTER (WHERE bucket = 'delayed') > $3 AS delayed_truncated,
460
+ count(*) FILTER (WHERE bucket = 'in_flight') > $3 AS in_flight_truncated,
461
+ count(*) FILTER (WHERE bucket = 'blocked') > $3 AS blocked_truncated,
462
+ count(*) FILTER (WHERE bucket = 'dead_lettered') > $3 AS dead_lettered_truncated,
463
463
  0::bigint AS admitted,
464
464
  0::bigint AS started,
465
465
  0::bigint AS completed,
@@ -493,34 +493,34 @@ function outboxProjectionQuery(schema: string): string {
493
493
  FROM ${table}
494
494
  WHERE delivered_at IS NULL AND failed_at IS NULL
495
495
  ORDER BY coalesce(next_attempt_at, created_at), created_at, event_id
496
- LIMIT $3
496
+ LIMIT $2
497
497
  ), dead_sample AS (
498
498
  SELECT 'dead_lettered'::text AS bucket, coalesce(failed_at, created_at) AS age_at
499
499
  FROM ${table}
500
500
  WHERE delivered_at IS NULL AND failed_at IS NOT NULL
501
501
  ORDER BY failed_at, event_id
502
- LIMIT $3
502
+ LIMIT $2
503
503
  ), sampled AS (
504
504
  SELECT * FROM pending_sample
505
505
  UNION ALL
506
506
  SELECT * FROM dead_sample
507
507
  )
508
508
  SELECT
509
- least(count(*) FILTER (WHERE bucket = 'ready'), $4) AS ready,
510
- least(count(*) FILTER (WHERE bucket = 'delayed'), $4) AS delayed,
511
- least(count(*) FILTER (WHERE bucket = 'in_flight'), $4) AS in_flight,
512
- least(count(*) FILTER (WHERE bucket = 'blocked'), $4) AS blocked,
513
- least(count(*) FILTER (WHERE bucket = 'dead_lettered'), $4) AS dead_lettered,
509
+ least(count(*) FILTER (WHERE bucket = 'ready'), $3) AS ready,
510
+ least(count(*) FILTER (WHERE bucket = 'delayed'), $3) AS delayed,
511
+ least(count(*) FILTER (WHERE bucket = 'in_flight'), $3) AS in_flight,
512
+ least(count(*) FILTER (WHERE bucket = 'blocked'), $3) AS blocked,
513
+ least(count(*) FILTER (WHERE bucket = 'dead_lettered'), $3) AS dead_lettered,
514
514
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'ready') AS oldest_ready_ms,
515
515
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'delayed') AS oldest_delayed_ms,
516
516
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'in_flight') AS oldest_in_flight_ms,
517
517
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'blocked') AS oldest_blocked_ms,
518
518
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'dead_lettered') AS oldest_dead_lettered_ms,
519
- count(*) FILTER (WHERE bucket <> 'dead_lettered') > $4 AS ready_truncated,
520
- count(*) FILTER (WHERE bucket <> 'dead_lettered') > $4 AS delayed_truncated,
521
- count(*) FILTER (WHERE bucket <> 'dead_lettered') > $4 AS in_flight_truncated,
522
- count(*) FILTER (WHERE bucket <> 'dead_lettered') > $4 AS blocked_truncated,
523
- count(*) FILTER (WHERE bucket = 'dead_lettered') > $4 AS dead_lettered_truncated,
519
+ count(*) FILTER (WHERE bucket <> 'dead_lettered') > $3 AS ready_truncated,
520
+ count(*) FILTER (WHERE bucket <> 'dead_lettered') > $3 AS delayed_truncated,
521
+ count(*) FILTER (WHERE bucket <> 'dead_lettered') > $3 AS in_flight_truncated,
522
+ count(*) FILTER (WHERE bucket <> 'dead_lettered') > $3 AS blocked_truncated,
523
+ count(*) FILTER (WHERE bucket = 'dead_lettered') > $3 AS dead_lettered_truncated,
524
524
  0::bigint AS admitted,
525
525
  0::bigint AS started,
526
526
  0::bigint AS completed,
@@ -556,23 +556,23 @@ function cleanupProjectionQuery(schema: string): string {
556
556
  FROM ${table}
557
557
  WHERE status <> 'deleted'
558
558
  ORDER BY available_at, created_at, provider, sandbox_id
559
- LIMIT $3
559
+ LIMIT $2
560
560
  )
561
561
  SELECT
562
- least(count(*) FILTER (WHERE bucket = 'ready'), $4) AS ready,
563
- least(count(*) FILTER (WHERE bucket = 'delayed'), $4) AS delayed,
564
- least(count(*) FILTER (WHERE bucket = 'in_flight'), $4) AS in_flight,
565
- least(count(*) FILTER (WHERE bucket = 'blocked'), $4) AS blocked,
562
+ least(count(*) FILTER (WHERE bucket = 'ready'), $3) AS ready,
563
+ least(count(*) FILTER (WHERE bucket = 'delayed'), $3) AS delayed,
564
+ least(count(*) FILTER (WHERE bucket = 'in_flight'), $3) AS in_flight,
565
+ least(count(*) FILTER (WHERE bucket = 'blocked'), $3) AS blocked,
566
566
  0::bigint AS dead_lettered,
567
567
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'ready') AS oldest_ready_ms,
568
568
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'delayed') AS oldest_delayed_ms,
569
569
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'in_flight') AS oldest_in_flight_ms,
570
570
  max(${intervalExpr('age_at')}) FILTER (WHERE bucket = 'blocked') AS oldest_blocked_ms,
571
571
  NULL::bigint AS oldest_dead_lettered_ms,
572
- count(*) > $4 AS ready_truncated,
573
- count(*) > $4 AS delayed_truncated,
574
- count(*) > $4 AS in_flight_truncated,
575
- count(*) > $4 AS blocked_truncated,
572
+ count(*) > $3 AS ready_truncated,
573
+ count(*) > $3 AS delayed_truncated,
574
+ count(*) > $3 AS in_flight_truncated,
575
+ count(*) > $3 AS blocked_truncated,
576
576
  false AS dead_lettered_truncated,
577
577
  0::bigint AS admitted,
578
578
  0::bigint AS started,
@@ -658,7 +658,7 @@ export async function readRuntimeQueueHealthFromClient(
658
658
  for (const queueName of queueNames) {
659
659
  const result = await client.query<CountRow>(
660
660
  absurdProjectionQuery(`r_${queueName}`),
661
- [now, now, sampleLimit + 1, sampleLimit],
661
+ [now, sampleLimit + 1, sampleLimit],
662
662
  );
663
663
  projections.push(
664
664
  projectionFromRow({
@@ -673,13 +673,11 @@ export async function readRuntimeQueueHealthFromClient(
673
673
  }
674
674
  const [outbox, cleanup] = await Promise.all([
675
675
  client.query<CountRow>(outboxProjectionQuery(schema), [
676
- now,
677
676
  now,
678
677
  sampleLimit + 1,
679
678
  sampleLimit,
680
679
  ]),
681
680
  client.query<CountRow>(cleanupProjectionQuery(schema), [
682
- now,
683
681
  now,
684
682
  sampleLimit + 1,
685
683
  sampleLimit,
@@ -68,6 +68,8 @@ export type ProductNotificationPresentation = {
68
68
  subjectKind: string;
69
69
  subjectId: string;
70
70
  severity?: ProductNotificationSeverity;
71
+ /** Selects an alternate email layout; absent means the standard one. */
72
+ emailFormat?: 'thank_you';
71
73
  };
72
74
 
73
75
  type ProductNotificationRuleInput = {
@@ -233,6 +235,31 @@ function formatCredits(value: number): string {
233
235
  return value.toLocaleString('en-US', { maximumFractionDigits: 2 });
234
236
  }
235
237
 
238
+ export const DEEPLINE_DOCS_URL = 'https://deepline.com/docs';
239
+
240
+ /** Points a newly paying workspace at the docs to get value from Deepline. */
241
+ const LEARN_DEEPLINE_DETAIL: NotificationText = [
242
+ 'To learn more about how to use Deepline, ',
243
+ { link: 'check out the docs', href: DEEPLINE_DOCS_URL },
244
+ '.',
245
+ ];
246
+
247
+ export const DEEPLINE_DEMO_URL =
248
+ 'https://calendly.com/d/csdb-85z-t6z/deepline-demo';
249
+
250
+ /**
251
+ * Growth welcomes lead with a demo call. One detail per line so the links read
252
+ * as a short list with the app's Lucide icons.
253
+ */
254
+ const GROWTH_LEARN_MORE_DETAILS: NotificationText[] = [
255
+ 'To learn more about Deepline:',
256
+ [{ icon: 'calendar' }, { link: 'Schedule a demo', href: DEEPLINE_DEMO_URL }],
257
+ [
258
+ { icon: 'book-open' },
259
+ { link: 'Check out the docs', href: DEEPLINE_DOCS_URL },
260
+ ],
261
+ ];
262
+
236
263
  /** One canonical registry owns every notification type's runtime and UI metadata. */
237
264
  const PRODUCT_NOTIFICATION_TYPE_REGISTRY = [
238
265
  {
@@ -546,6 +573,108 @@ const PRODUCT_NOTIFICATION_TYPE_REGISTRY = [
546
573
  },
547
574
  },
548
575
  },
576
+ {
577
+ id: 'credits.purchased',
578
+ eventName: ProductNotificationEvent.CreditsPurchaseCompleted,
579
+ requiredFilter: {},
580
+ allowedTriggers: ['threshold_crossing'],
581
+ catalog: {
582
+ name: 'Credit purchase thank-you',
583
+ category: 'credits',
584
+ description:
585
+ 'Thank the workspace when a credit purchase of at least the chosen size completes.',
586
+ allowMultiple: false,
587
+ editor: {
588
+ kind: 'threshold',
589
+ label: 'Minimum credits',
590
+ defaultThreshold: 500,
591
+ minimum: 0,
592
+ suffix: 'credits',
593
+ },
594
+ },
595
+ rule: {
596
+ default: { ruleKey: 'default.credits_purchased', groupKey: 'default' },
597
+ create: (input, base, editor) => ({
598
+ ...base,
599
+ repeatMode: 'every_match',
600
+ trigger: {
601
+ kind: 'threshold_crossing',
602
+ direction: 'at_or_above',
603
+ threshold: input.threshold ?? defaultThreshold(editor),
604
+ unit: 'credits',
605
+ },
606
+ }),
607
+ validate: (trigger) => {
608
+ if (
609
+ trigger.kind !== 'threshold_crossing' ||
610
+ trigger.direction !== 'at_or_above' ||
611
+ trigger.unit !== 'credits'
612
+ ) {
613
+ throw new Error(
614
+ 'Credit purchase rules must reach a purchased credit amount.',
615
+ );
616
+ }
617
+ if (!Number.isFinite(trigger.threshold) || trigger.threshold < 0) {
618
+ throw new Error(
619
+ 'Credit purchase thresholds must be finite and nonnegative.',
620
+ );
621
+ }
622
+ },
623
+ present: ({ payload }) => ({
624
+ title: 'Thank you for your credit purchase',
625
+ summary: [
626
+ 'Your ',
627
+ { workspace: true },
628
+ ' added ',
629
+ { strong: `${formatCredits(Number(payload.credits))} credits` },
630
+ '. Thank you for supporting Deepline.',
631
+ ],
632
+ details: [LEARN_DEEPLINE_DETAIL],
633
+ emailFormat: 'thank_you',
634
+ targetUrl: String(payload.targetUrl),
635
+ subjectKind: 'workspace',
636
+ subjectId: String(payload.accountId),
637
+ }),
638
+ },
639
+ },
640
+ {
641
+ id: 'plan.purchased',
642
+ eventName: ProductNotificationEvent.BillingPlanPurchased,
643
+ requiredFilter: {},
644
+ allowedTriggers: ['each_match'],
645
+ catalog: {
646
+ name: 'Plan welcome',
647
+ category: 'credits',
648
+ description: 'Thank the workspace when it purchases a plan.',
649
+ allowMultiple: false,
650
+ editor: { kind: 'none' },
651
+ },
652
+ rule: {
653
+ default: { ruleKey: 'default.plan_purchased', groupKey: 'default' },
654
+ create: (_input, base) => ({
655
+ ...base,
656
+ repeatMode: 'every_match',
657
+ trigger: { kind: 'each_match' },
658
+ }),
659
+ present: ({ payload }) => ({
660
+ title: `Welcome to ${payload.planName}`,
661
+ summary: [
662
+ 'Your ',
663
+ { workspace: true },
664
+ ` is now on the ${payload.planName} plan. Thank you for supporting Deepline.`,
665
+ ],
666
+ // No credit-lifetime claims: they differ by plan.
667
+ details:
668
+ payload.offerKey === 'growth-v1'
669
+ ? GROWTH_LEARN_MORE_DETAILS
670
+ : [LEARN_DEEPLINE_DETAIL],
671
+ emailFormat: 'thank_you',
672
+ targetUrl: String(payload.targetUrl),
673
+ subjectKind: 'workspace',
674
+ subjectId: String(payload.accountId),
675
+ }),
676
+ },
677
+ },
549
678
  {
550
679
  id: 'play.failed',
551
680
  eventName: ProductNotificationEvent.PlayRunCompleted,
@@ -1,7 +1,17 @@
1
1
  /** Type-owned content, independent of email markup and persisted audit strings. */
2
2
  export type NotificationText =
3
3
  | string
4
- | readonly (string | { strong: string } | { workspace: true })[];
4
+ | readonly (
5
+ | string
6
+ | { strong: string }
7
+ | { workspace: true }
8
+ /** An absolute https link authored in code, never customer input. */
9
+ | { link: string; href: string }
10
+ /** A decorative brand icon; plain text omits it. */
11
+ | { icon: NotificationIcon }
12
+ )[];
13
+
14
+ export type NotificationIcon = 'calendar' | 'book-open';
5
15
 
6
16
  export function notificationText(
7
17
  content: NotificationText,
@@ -12,6 +22,8 @@ export function notificationText(
12
22
  .map((part) => {
13
23
  if (typeof part === 'string') return part;
14
24
  if ('strong' in part) return part.strong;
25
+ if ('link' in part) return `${part.link} (${part.href})`;
26
+ if ('icon' in part) return '';
15
27
  return workspaceName ? `workspace ${workspaceName}` : 'workspace';
16
28
  })
17
29
  .join('');
@@ -14,7 +14,7 @@ import {
14
14
  * they create sparse copy-on-write overrides.
15
15
  */
16
16
  export const CURRENT_PRODUCT_NOTIFICATION_DEFAULT_POLICY = {
17
- version: 5,
17
+ version: 6,
18
18
  groups: [
19
19
  {
20
20
  groupKey: 'default',
@@ -32,10 +32,24 @@ type ProductNotificationEmailContent = {
32
32
  details: NotificationText[];
33
33
  targetUrl?: string;
34
34
  severity?: 'success' | 'warning' | 'error';
35
+ /**
36
+ * `thank_you` is the standard layout without the notification group header,
37
+ * with details shown as a compact list instead of divided rows. Kept as its
38
+ * own format so these emails can diverge later.
39
+ */
40
+ emailFormat?: 'thank_you';
35
41
  };
36
42
 
37
43
  const MAX_RECIPIENTS = 50;
38
44
 
45
+ /**
46
+ * Lucide icons rasterized to 48px PNGs in the light-theme muted foreground
47
+ * color, served from `apps/deepline-api/public/brand/email`. Email clients do
48
+ * not render inline SVG.
49
+ */
50
+ export const NOTIFICATION_ICON_BASE_URL =
51
+ 'https://code.deepline.com/brand/email';
52
+
39
53
  const SEVERITY_STYLE: Record<
40
54
  NonNullable<ProductNotificationEmailContent['severity']>,
41
55
  { bg: string; text: string; label: string }
@@ -74,6 +88,14 @@ function unsubscribeUrl(value: string): string {
74
88
  return url.toString();
75
89
  }
76
90
 
91
+ function externalLinkUrl(value: string): string {
92
+ const url = new URL(value);
93
+ if (url.protocol !== 'https:') {
94
+ throw new Error('Notification content links must use HTTPS.');
95
+ }
96
+ return url.toString();
97
+ }
98
+
77
99
  function contentHtml(
78
100
  content: NotificationText,
79
101
  workspaceName?: string,
@@ -84,6 +106,10 @@ function contentHtml(
84
106
  if (typeof part === 'string') return escapeHtml(part);
85
107
  if ('strong' in part)
86
108
  return `<strong>${escapeHtml(part.strong)}</strong>`;
109
+ if ('link' in part)
110
+ return `<a href="${escapeHtml(externalLinkUrl(part.href))}" style="color:#0a0a0a;text-decoration:underline">${escapeHtml(part.link)}</a>`;
111
+ if ('icon' in part)
112
+ return `<img src="${NOTIFICATION_ICON_BASE_URL}/icon-${part.icon}.png" width="16" height="16" alt="" style="display:inline-block;vertical-align:-3px;margin-right:8px;border:0">`;
87
113
  return workspaceName
88
114
  ? `workspace <strong>${escapeHtml(workspaceName)}</strong>`
89
115
  : 'workspace';
@@ -95,8 +121,12 @@ function detailHtml(
95
121
  detail: NotificationText,
96
122
  workspaceName: string | undefined,
97
123
  borderBottom: boolean,
124
+ /** List-style rows: no dividers, tighter spacing. */
125
+ compact = false,
98
126
  ): string {
99
- const border = borderBottom ? ';border-bottom:1px solid #f0f0f0' : '';
127
+ const border =
128
+ borderBottom && !compact ? ';border-bottom:1px solid #f0f0f0' : '';
129
+ const padding = compact ? '5px 0' : '12px 0';
100
130
  if (
101
131
  Array.isArray(detail) &&
102
132
  detail.length === 2 &&
@@ -109,7 +139,7 @@ function detailHtml(
109
139
  const value = typeof detail[1] === 'string' ? detail[1] : detail[1].strong;
110
140
  return `<table role="presentation" width="100%" cellspacing="0" cellpadding="0"><tr><td style="padding:12px 0${border};font-size:14px;color:#0a0a0a">${escapeHtml(detail[0].trimEnd())}</td><td align="right" style="padding:12px 0${border};font-size:14px;color:#0a0a0a${typeof detail[1] === 'string' ? '' : ';font-weight:600'}">${escapeHtml(value)}</td></tr></table>`;
111
141
  }
112
- return `<p style="margin:0;padding:12px 0${border};font-size:14px;color:#0a0a0a">${contentHtml(detail, workspaceName)}</p>`;
142
+ return `<p style="margin:0;padding:${padding}${border};font-size:14px;color:#0a0a0a">${contentHtml(detail, workspaceName)}</p>`;
113
143
  }
114
144
 
115
145
  export function renderProductNotificationEmail(
@@ -155,9 +185,12 @@ export function renderProductNotificationEmail(
155
185
  const oneClickOptOutUrl = input.oneClickUnsubscribeUrl
156
186
  ? unsubscribeUrl(input.oneClickUnsubscribeUrl)
157
187
  : optOutUrl;
188
+ const showGroupHeader = input.content.emailFormat !== 'thank_you';
158
189
  const text = [
159
190
  input.content.title,
160
- `Notification group: ${input.content.groupName}`,
191
+ ...(showGroupHeader
192
+ ? [`Notification group: ${input.content.groupName}`]
193
+ : []),
161
194
  ...(targetUrl ? [`Open Deepline: ${targetUrl}`] : []),
162
195
  '',
163
196
  notificationText(input.content.summary, input.workspaceName),
@@ -184,16 +217,25 @@ export function renderProductNotificationEmail(
184
217
  `<td align="left" valign="middle"><a href="${escapeHtml(logoUrl)}" style="display:inline-block;text-decoration:none"><img src="https://code.deepline.com/brand/logo_full_dark.png" width="120" height="auto" alt="Deepline" style="display:block;border:0"></a></td>`,
185
218
  `<td align="right" valign="middle">${severityBadge}</td>`,
186
219
  '</tr></table>',
187
- '<p style="margin:0 0 16px">',
188
- `<span style="font-size:12px;color:#a3a3a3;letter-spacing:.06em;text-transform:uppercase">${escapeHtml(input.content.groupName)}</span>`,
189
- '</p>',
220
+ ...(showGroupHeader
221
+ ? [
222
+ '<p style="margin:0 0 16px">',
223
+ `<span style="font-size:12px;color:#a3a3a3;letter-spacing:.06em;text-transform:uppercase">${escapeHtml(input.content.groupName)}</span>`,
224
+ '</p>',
225
+ ]
226
+ : []),
190
227
  `<h1 style="margin:0 0 8px;font-size:22px;font-weight:600;color:#0a0a0a;line-height:1.3;letter-spacing:-0.02em">${escapeHtml(input.content.title)}</h1>`,
191
228
  `<p style="margin:0 0 24px;font-size:15px;line-height:1.6;color:#525252">${contentHtml(input.content.summary, input.workspaceName)}</p>`,
192
229
  ...(detailCount > 0
193
230
  ? [
194
- '<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="border:1px solid #e5e5e5;border-radius:10px;margin:0 0 28px"><tr><td style="padding:4px 18px">',
231
+ `<table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="border:1px solid #e5e5e5;border-radius:10px;margin:0 0 28px"><tr><td style="padding:${showGroupHeader ? '4px' : '11px'} 18px">`,
195
232
  ...input.content.details.map((detail, index) =>
196
- detailHtml(detail, input.workspaceName, index < detailCount - 1),
233
+ detailHtml(
234
+ detail,
235
+ input.workspaceName,
236
+ index < detailCount - 1,
237
+ !showGroupHeader,
238
+ ),
197
239
  ),
198
240
  '</td></tr></table>',
199
241
  ]
@@ -15,7 +15,9 @@ export const PRODUCT_NOTIFICATION_MAX_SOURCE_CLOCK_SKEW_MS = 5 * 60 * 1_000;
15
15
 
16
16
  export const ProductNotificationEvent = {
17
17
  CreditsBalanceChanged: 'credits.balance.changed',
18
+ CreditsPurchaseCompleted: 'credits.purchase.completed',
18
19
  CreditsUsageRecorded: 'credits.usage.recorded',
20
+ BillingPlanPurchased: 'billing.plan.purchased',
19
21
  MonitorLifecycleChanged: 'monitor.lifecycle.changed',
20
22
  PlayLifecycleChanged: 'play.lifecycle.changed',
21
23
  PlayRunCompleted: 'play.run.completed',
@@ -31,6 +33,7 @@ export type ProductNotificationEvent =
31
33
 
32
34
  export const ProductNotificationProducerKey = {
33
35
  BillingCredits: 'billing.credits',
36
+ BillingPlans: 'billing.plans',
34
37
  BillingUsage: 'billing.usage',
35
38
  MonitorsLifecycle: 'monitors.lifecycle',
36
39
  PlaysLifecycle: 'plays.lifecycle',
@@ -53,6 +56,8 @@ export const PRODUCT_NOTIFICATION_EVENT_NAMES = [
53
56
  ProductNotificationEvent.PlayRunCompleted,
54
57
  ProductNotificationEvent.WebhookDeliveryCompleted,
55
58
  ProductNotificationEvent.CreditsUsageRecorded,
59
+ ProductNotificationEvent.CreditsPurchaseCompleted,
60
+ ProductNotificationEvent.BillingPlanPurchased,
56
61
  ] as const;
57
62
 
58
63
  export const PRODUCT_NOTIFICATION_COMPATIBILITY_EVENT_NAMES = [
@@ -175,6 +180,33 @@ export const PRODUCT_NOTIFICATION_EVENT_DEFINITIONS: readonly ProductNotificatio
175
180
  allowedTriggers: ['threshold_crossing'],
176
181
  allowedThresholdUnits: ['percent', 'usage'],
177
182
  },
183
+ {
184
+ // One paid credit purchase (manual or automatic recharge) settled.
185
+ eventName: ProductNotificationEvent.CreditsPurchaseCompleted,
186
+ producerKey: ProductNotificationProducerKey.BillingCredits,
187
+ requiredFacts: {
188
+ accountId: 'string',
189
+ credits: 'number',
190
+ targetUrl: 'string',
191
+ },
192
+ orderingSubjectFact: 'accountId',
193
+ allowedTriggers: ['threshold_crossing'],
194
+ allowedThresholdUnits: ['credits'],
195
+ },
196
+ {
197
+ // The workspace moved onto a paid acquisition offer.
198
+ eventName: ProductNotificationEvent.BillingPlanPurchased,
199
+ producerKey: ProductNotificationProducerKey.BillingPlans,
200
+ requiredFacts: {
201
+ accountId: 'string',
202
+ offerKey: 'string',
203
+ planName: 'string',
204
+ targetUrl: 'string',
205
+ },
206
+ optionalFacts: { previousOfferKey: 'string' },
207
+ orderingSubjectFact: 'accountId',
208
+ allowedTriggers: ['each_match'],
209
+ },
178
210
  ...PRODUCT_NOTIFICATION_COMPATIBILITY_EVENT_NAMES.map((eventName) => ({
179
211
  eventName,
180
212
  producerKey: ProductNotificationProducerKey.LegacyProductNotifications,
@@ -396,6 +428,13 @@ export function adaptProductNotificationObservation(input: {
396
428
  unit: 'credits',
397
429
  };
398
430
  }
431
+ if (event.eventName === ProductNotificationEvent.CreditsPurchaseCompleted) {
432
+ return {
433
+ kind: 'threshold_state',
434
+ currentValue: Number(event.payload.credits),
435
+ unit: 'credits',
436
+ };
437
+ }
399
438
  const limit = Number(event.payload.limit);
400
439
  if (event.eventName === ProductNotificationEvent.CreditsUsageRecorded) {
401
440
  if (!(limit > 0)) throw new Error('Monthly usage limit must be positive.');
package/dist/cli/index.js CHANGED
@@ -1298,7 +1298,7 @@ var SDK_RELEASE = {
1298
1298
  // getters keep their established compatibility behavior.
1299
1299
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1300
1300
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
1301
- version: "0.3.108",
1301
+ version: "0.3.110",
1302
1302
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
1303
1303
  packageCapabilities: {
1304
1304
  updatePreferences: 1
@@ -1271,7 +1271,7 @@ var SDK_RELEASE = {
1271
1271
  // getters keep their established compatibility behavior.
1272
1272
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
1273
1273
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
1274
- version: "0.3.108",
1274
+ version: "0.3.110",
1275
1275
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
1276
1276
  packageCapabilities: {
1277
1277
  updatePreferences: 1
package/dist/index.d.mts CHANGED
@@ -318,7 +318,17 @@ type NotificationText = string | readonly (string | {
318
318
  strong: string;
319
319
  } | {
320
320
  workspace: true;
321
+ }
322
+ /** An absolute https link authored in code, never customer input. */
323
+ | {
324
+ link: string;
325
+ href: string;
326
+ }
327
+ /** A decorative brand icon; plain text omits it. */
328
+ | {
329
+ icon: NotificationIcon;
321
330
  })[];
331
+ type NotificationIcon = 'calendar' | 'book-open';
322
332
 
323
333
  /**
324
334
  * The sections the configuration surface skims by. Declared here so UI, API,
@@ -368,6 +378,8 @@ type ProductNotificationPresentation = {
368
378
  subjectKind: string;
369
379
  subjectId: string;
370
380
  severity?: ProductNotificationSeverity;
381
+ /** Selects an alternate email layout; absent means the standard one. */
382
+ emailFormat?: 'thank_you';
371
383
  };
372
384
  type ProductNotificationRuleInput = {
373
385
  ruleKey: string;
@@ -698,6 +710,143 @@ declare const PRODUCT_NOTIFICATION_TYPE_REGISTRY: readonly [{
698
710
  severity: "warning";
699
711
  };
700
712
  };
713
+ }, {
714
+ readonly id: "credits.purchased";
715
+ readonly eventName: "credits.purchase.completed";
716
+ readonly requiredFilter: {};
717
+ readonly allowedTriggers: ["threshold_crossing"];
718
+ readonly catalog: {
719
+ readonly name: "Credit purchase thank-you";
720
+ readonly category: "credits";
721
+ readonly description: "Thank the workspace when a credit purchase of at least the chosen size completes.";
722
+ readonly allowMultiple: false;
723
+ readonly editor: {
724
+ readonly kind: "threshold";
725
+ readonly label: "Minimum credits";
726
+ readonly defaultThreshold: 500;
727
+ readonly minimum: 0;
728
+ readonly suffix: "credits";
729
+ };
730
+ };
731
+ readonly rule: {
732
+ readonly default: {
733
+ readonly ruleKey: "default.credits_purchased";
734
+ readonly groupKey: "default";
735
+ };
736
+ readonly create: (input: ProductNotificationRuleInput, base: ProductNotificationRuleBase, editor: {
737
+ kind: "threshold";
738
+ label: string;
739
+ defaultThreshold: number;
740
+ minimum: number;
741
+ maximum?: number;
742
+ suffix?: string;
743
+ /**
744
+ * When set, editors show and accept `remainingOf - threshold` (for
745
+ * example percent remaining) while the stored trigger stays in the
746
+ * runtime's "used" terms. Bounds and defaults are stored values.
747
+ */
748
+ remainingOf?: number;
749
+ } | {
750
+ kind: "play_failure";
751
+ defaultFailureCount: number;
752
+ } | {
753
+ kind: "play_scope";
754
+ } | {
755
+ kind: "none";
756
+ }) => {
757
+ repeatMode: "every_match";
758
+ trigger: {
759
+ kind: "threshold_crossing";
760
+ direction: "at_or_above";
761
+ threshold: number;
762
+ unit: string;
763
+ };
764
+ groupKey: string;
765
+ enabled: boolean;
766
+ conditionGeneration?: number | undefined;
767
+ ruleKey: string;
768
+ notificationType: string;
769
+ eventName: string;
770
+ eventFilter: ProductNotificationFacts;
771
+ };
772
+ readonly validate: (trigger: ProductNotificationTrigger) => void;
773
+ readonly present: ({ payload }: {
774
+ payload: ProductNotificationFacts;
775
+ trigger: ProductNotificationTrigger;
776
+ occurredAt?: number;
777
+ }) => {
778
+ title: string;
779
+ summary: (string | {
780
+ workspace: true;
781
+ strong?: undefined;
782
+ } | {
783
+ strong: string;
784
+ workspace?: undefined;
785
+ })[];
786
+ details: (readonly (string | {
787
+ strong: string;
788
+ } | {
789
+ workspace: true;
790
+ } | {
791
+ link: string;
792
+ href: string;
793
+ } | {
794
+ icon: NotificationIcon;
795
+ })[])[];
796
+ emailFormat: "thank_you";
797
+ targetUrl: string;
798
+ subjectKind: string;
799
+ subjectId: string;
800
+ };
801
+ };
802
+ }, {
803
+ readonly id: "plan.purchased";
804
+ readonly eventName: "billing.plan.purchased";
805
+ readonly requiredFilter: {};
806
+ readonly allowedTriggers: ["each_match"];
807
+ readonly catalog: {
808
+ readonly name: "Plan welcome";
809
+ readonly category: "credits";
810
+ readonly description: "Thank the workspace when it purchases a plan.";
811
+ readonly allowMultiple: false;
812
+ readonly editor: {
813
+ readonly kind: "none";
814
+ };
815
+ };
816
+ readonly rule: {
817
+ readonly default: {
818
+ readonly ruleKey: "default.plan_purchased";
819
+ readonly groupKey: "default";
820
+ };
821
+ readonly create: (_input: ProductNotificationRuleInput, base: ProductNotificationRuleBase) => {
822
+ repeatMode: "every_match";
823
+ trigger: {
824
+ kind: "each_match";
825
+ };
826
+ groupKey: string;
827
+ enabled: boolean;
828
+ conditionGeneration?: number | undefined;
829
+ ruleKey: string;
830
+ notificationType: string;
831
+ eventName: string;
832
+ eventFilter: ProductNotificationFacts;
833
+ };
834
+ readonly present: ({ payload }: {
835
+ payload: ProductNotificationFacts;
836
+ trigger: ProductNotificationTrigger;
837
+ occurredAt?: number;
838
+ }) => {
839
+ title: string;
840
+ summary: (string | {
841
+ workspace: true;
842
+ })[];
843
+ details: NotificationText[];
844
+ emailFormat: "thank_you";
845
+ targetUrl: string;
846
+ subjectKind: string;
847
+ subjectId: string;
848
+ };
849
+ };
701
850
  }, {
702
851
  readonly id: "play.failed";
703
852
  readonly eventName: "play.run.completed";
package/dist/index.d.ts CHANGED
@@ -318,7 +318,17 @@ type NotificationText = string | readonly (string | {
318
318
  strong: string;
319
319
  } | {
320
320
  workspace: true;
321
+ }
322
+ /** An absolute https link authored in code, never customer input. */
323
+ | {
324
+ link: string;
325
+ href: string;
326
+ }
327
+ /** A decorative brand icon; plain text omits it. */
328
+ | {
329
+ icon: NotificationIcon;
321
330
  })[];
331
+ type NotificationIcon = 'calendar' | 'book-open';
322
332
 
323
333
  /**
324
334
  * The sections the configuration surface skims by. Declared here so UI, API,
@@ -368,6 +378,8 @@ type ProductNotificationPresentation = {
368
378
  subjectKind: string;
369
379
  subjectId: string;
370
380
  severity?: ProductNotificationSeverity;
381
+ /** Selects an alternate email layout; absent means the standard one. */
382
+ emailFormat?: 'thank_you';
371
383
  };
372
384
  type ProductNotificationRuleInput = {
373
385
  ruleKey: string;
@@ -698,6 +710,143 @@ declare const PRODUCT_NOTIFICATION_TYPE_REGISTRY: readonly [{
698
710
  severity: "warning";
699
711
  };
700
712
  };
713
+ }, {
714
+ readonly id: "credits.purchased";
715
+ readonly eventName: "credits.purchase.completed";
716
+ readonly requiredFilter: {};
717
+ readonly allowedTriggers: ["threshold_crossing"];
718
+ readonly catalog: {
719
+ readonly name: "Credit purchase thank-you";
720
+ readonly category: "credits";
721
+ readonly description: "Thank the workspace when a credit purchase of at least the chosen size completes.";
722
+ readonly allowMultiple: false;
723
+ readonly editor: {
724
+ readonly kind: "threshold";
725
+ readonly label: "Minimum credits";
726
+ readonly defaultThreshold: 500;
727
+ readonly minimum: 0;
728
+ readonly suffix: "credits";
729
+ };
730
+ };
731
+ readonly rule: {
732
+ readonly default: {
733
+ readonly ruleKey: "default.credits_purchased";
734
+ readonly groupKey: "default";
735
+ };
736
+ readonly create: (input: ProductNotificationRuleInput, base: ProductNotificationRuleBase, editor: {
737
+ kind: "threshold";
738
+ label: string;
739
+ defaultThreshold: number;
740
+ minimum: number;
741
+ maximum?: number;
742
+ suffix?: string;
743
+ /**
744
+ * When set, editors show and accept `remainingOf - threshold` (for
745
+ * example percent remaining) while the stored trigger stays in the
746
+ * runtime's "used" terms. Bounds and defaults are stored values.
747
+ */
748
+ remainingOf?: number;
749
+ } | {
750
+ kind: "play_failure";
751
+ defaultFailureCount: number;
752
+ } | {
753
+ kind: "play_scope";
754
+ } | {
755
+ kind: "none";
756
+ }) => {
757
+ repeatMode: "every_match";
758
+ trigger: {
759
+ kind: "threshold_crossing";
760
+ direction: "at_or_above";
761
+ threshold: number;
762
+ unit: string;
763
+ };
764
+ groupKey: string;
765
+ enabled: boolean;
766
+ conditionGeneration?: number | undefined;
767
+ ruleKey: string;
768
+ notificationType: string;
769
+ eventName: string;
770
+ eventFilter: ProductNotificationFacts;
771
+ };
772
+ readonly validate: (trigger: ProductNotificationTrigger) => void;
773
+ readonly present: ({ payload }: {
774
+ payload: ProductNotificationFacts;
775
+ trigger: ProductNotificationTrigger;
776
+ occurredAt?: number;
777
+ }) => {
778
+ title: string;
779
+ summary: (string | {
780
+ workspace: true;
781
+ strong?: undefined;
782
+ } | {
783
+ strong: string;
784
+ workspace?: undefined;
785
+ })[];
786
+ details: (readonly (string | {
787
+ strong: string;
788
+ } | {
789
+ workspace: true;
790
+ } | {
791
+ link: string;
792
+ href: string;
793
+ } | {
794
+ icon: NotificationIcon;
795
+ })[])[];
796
+ emailFormat: "thank_you";
797
+ targetUrl: string;
798
+ subjectKind: string;
799
+ subjectId: string;
800
+ };
801
+ };
802
+ }, {
803
+ readonly id: "plan.purchased";
804
+ readonly eventName: "billing.plan.purchased";
805
+ readonly requiredFilter: {};
806
+ readonly allowedTriggers: ["each_match"];
807
+ readonly catalog: {
808
+ readonly name: "Plan welcome";
809
+ readonly category: "credits";
810
+ readonly description: "Thank the workspace when it purchases a plan.";
811
+ readonly allowMultiple: false;
812
+ readonly editor: {
813
+ readonly kind: "none";
814
+ };
815
+ };
816
+ readonly rule: {
817
+ readonly default: {
818
+ readonly ruleKey: "default.plan_purchased";
819
+ readonly groupKey: "default";
820
+ };
821
+ readonly create: (_input: ProductNotificationRuleInput, base: ProductNotificationRuleBase) => {
822
+ repeatMode: "every_match";
823
+ trigger: {
824
+ kind: "each_match";
825
+ };
826
+ groupKey: string;
827
+ enabled: boolean;
828
+ conditionGeneration?: number | undefined;
829
+ ruleKey: string;
830
+ notificationType: string;
831
+ eventName: string;
832
+ eventFilter: ProductNotificationFacts;
833
+ };
834
+ readonly present: ({ payload }: {
835
+ payload: ProductNotificationFacts;
836
+ trigger: ProductNotificationTrigger;
837
+ occurredAt?: number;
838
+ }) => {
839
+ title: string;
840
+ summary: (string | {
841
+ workspace: true;
842
+ })[];
843
+ details: NotificationText[];
844
+ emailFormat: "thank_you";
845
+ targetUrl: string;
846
+ subjectKind: string;
847
+ subjectId: string;
848
+ };
849
+ };
701
850
  }, {
702
851
  readonly id: "play.failed";
703
852
  readonly eventName: "play.run.completed";
package/dist/index.js CHANGED
@@ -860,7 +860,7 @@ var SDK_RELEASE = {
860
860
  // getters keep their established compatibility behavior.
861
861
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
862
862
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
863
- version: "0.3.108",
863
+ version: "0.3.110",
864
864
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
865
865
  packageCapabilities: {
866
866
  updatePreferences: 1
package/dist/index.mjs CHANGED
@@ -768,7 +768,7 @@ var SDK_RELEASE = {
768
768
  // getters keep their established compatibility behavior.
769
769
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
770
770
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
771
- version: "0.3.108",
771
+ version: "0.3.110",
772
772
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
773
773
  packageCapabilities: {
774
774
  updatePreferences: 1
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.108";
152
+ readonly version: "0.3.110";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.d.ts CHANGED
@@ -149,7 +149,7 @@ type SdkRelease = {
149
149
  supportPolicy: SdkSupportPolicy;
150
150
  };
151
151
  declare const SDK_RELEASE: {
152
- readonly version: "0.3.108";
152
+ readonly version: "0.3.110";
153
153
  readonly updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.";
154
154
  readonly packageCapabilities: {
155
155
  readonly updatePreferences: 1;
package/dist/release.js CHANGED
@@ -74,7 +74,7 @@ var SDK_RELEASE = {
74
74
  // getters keep their established compatibility behavior.
75
75
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
76
76
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
77
- version: "0.3.108",
77
+ version: "0.3.110",
78
78
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
79
79
  packageCapabilities: {
80
80
  updatePreferences: 1
package/dist/release.mjs CHANGED
@@ -48,7 +48,7 @@ var SDK_RELEASE = {
48
48
  // getters keep their established compatibility behavior.
49
49
  // 0.3.1 deprecated the legacy `deepline enrich` command in favor of Plays.
50
50
  // 0.3.90 is the first deliberately versioned SDK release for API v3.
51
- version: "0.3.108",
51
+ version: "0.3.110",
52
52
  updateSummary: "Play grep now returns concise results with the matched fields and terms. Automatic CLI updates are enabled by default; use `deepline settings autoupdate off` to opt out, `deepline settings autoupdate on` to re-enable updates, or `deepline settings autoupdate pin <version>` to hold an exact release.",
53
53
  packageCapabilities: {
54
54
  updatePreferences: 1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "deepline",
3
- "version": "0.3.108",
3
+ "version": "0.3.110",
4
4
  "description": "GTM data CLI and TypeScript SDK for coding agents",
5
5
  "license": "MIT",
6
6
  "homepage": "https://code.deepline.com",