@oxygen-agent/cli 1.1011.98 → 1.1011.322

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.
Files changed (74) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +25 -5
  3. package/dist/column-run-notices.d.ts +16 -0
  4. package/dist/column-run-notices.js +57 -0
  5. package/dist/command-manifest.js +10 -1
  6. package/dist/connectors-commands.d.ts +11 -0
  7. package/dist/connectors-commands.js +63 -0
  8. package/dist/help.d.ts +27 -1
  9. package/dist/help.js +112 -29
  10. package/dist/http-client.js +8 -0
  11. package/dist/index.js +362 -130
  12. package/node_modules/@oxygen/cli-ugc/dist/commands.js +20 -69
  13. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +6 -4
  14. package/node_modules/@oxygen/shared/dist/byok-connect.js +11 -4
  15. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +131 -1
  16. package/node_modules/@oxygen/shared/dist/capability-discovery.js +325 -30
  17. package/node_modules/@oxygen/shared/dist/collab.d.ts +1 -1
  18. package/node_modules/@oxygen/shared/dist/collab.js +12 -0
  19. package/node_modules/@oxygen/shared/dist/column-decision.js +7 -3
  20. package/node_modules/@oxygen/shared/dist/column-output-fields.js +45 -3
  21. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.d.ts +20 -13
  22. package/node_modules/@oxygen/shared/dist/company-enrichment-fields.js +29 -13
  23. package/node_modules/@oxygen/shared/dist/contracts/connectors.d.ts +23 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/connectors.js +34 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +0 -6
  26. package/node_modules/@oxygen/shared/dist/contracts/operations.js +8 -1
  27. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +1 -1
  28. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.d.ts +2 -2
  29. package/node_modules/@oxygen/shared/dist/copilot-skills.generated.js +2 -2
  30. package/node_modules/@oxygen/shared/dist/credit-guidance.js +12 -1
  31. package/node_modules/@oxygen/shared/dist/enrichment-intents.d.ts +37 -0
  32. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +79 -44
  33. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +5 -0
  34. package/node_modules/@oxygen/shared/dist/feature-gates.js +6 -0
  35. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +57 -8
  36. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +74 -13
  37. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +7 -0
  38. package/node_modules/@oxygen/shared/dist/linkedin-url.js +9 -0
  39. package/node_modules/@oxygen/shared/dist/log.d.ts +1 -0
  40. package/node_modules/@oxygen/shared/dist/log.js +15 -0
  41. package/node_modules/@oxygen/shared/dist/mcp-event-contract.d.ts +597 -0
  42. package/node_modules/@oxygen/shared/dist/mcp-event-contract.js +713 -0
  43. package/node_modules/@oxygen/shared/dist/mcp-events-enabled.d.ts +2 -0
  44. package/node_modules/@oxygen/shared/dist/mcp-events-enabled.js +14 -0
  45. package/node_modules/@oxygen/shared/dist/operational-telemetry.d.ts +1 -0
  46. package/node_modules/@oxygen/shared/dist/operational-telemetry.js +6 -2
  47. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +7 -3
  48. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +22 -1
  49. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +3 -7
  50. package/node_modules/@oxygen/shared/dist/plan-limits.js +2 -14
  51. package/node_modules/@oxygen/shared/dist/select-options.d.ts +6 -0
  52. package/node_modules/@oxygen/shared/dist/select-options.js +6 -1
  53. package/node_modules/@oxygen/shared/dist/sending-schedule.d.ts +39 -0
  54. package/node_modules/@oxygen/shared/dist/sending-schedule.js +41 -0
  55. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.d.ts +19 -0
  56. package/node_modules/@oxygen/shared/dist/sequence-lifecycle.js +20 -0
  57. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.d.ts +35 -0
  58. package/node_modules/@oxygen/shared/dist/sequence-sender-variables.js +63 -0
  59. package/node_modules/@oxygen/shared/dist/sequences.d.ts +13 -7
  60. package/node_modules/@oxygen/shared/dist/sequences.js +26 -0
  61. package/node_modules/@oxygen/shared/dist/signup-lead-payload.d.ts +2 -0
  62. package/node_modules/@oxygen/shared/dist/signup-lead-payload.js +5 -0
  63. package/node_modules/@oxygen/shared/dist/social-post-metrics-series.js +1 -15
  64. package/node_modules/@oxygen/shared/dist/standard-webhooks.d.ts +51 -0
  65. package/node_modules/@oxygen/shared/dist/standard-webhooks.js +124 -0
  66. package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
  67. package/node_modules/@oxygen/shared/dist/tags.js +13 -1
  68. package/node_modules/@oxygen/shared/dist/ugc.d.ts +11 -7
  69. package/node_modules/@oxygen/shared/dist/ugc.js +14 -6
  70. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  71. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  72. package/node_modules/@oxygen/shared/dist/zoned-time.d.ts +39 -0
  73. package/node_modules/@oxygen/shared/dist/zoned-time.js +81 -0
  74. package/package.json +1 -1
@@ -0,0 +1,124 @@
1
+ import { createHmac, timingSafeEqual } from "node:crypto";
2
+ // Standard Webhooks symmetric signatures for the webhooks OXYGEN SENDS: MCP
3
+ // Events deliveries and their callback challenges (OXP-11.5.17).
4
+ //
5
+ // The signed content is `${webhook-id}.${webhook-timestamp}.${body}`, and a
6
+ // signature is `v1,<base64 HMAC-SHA256>` keyed by the base64 value after the
7
+ // `whsec_` prefix. `webhook-signature` is a space-delimited list, so a rotation
8
+ // window carries one signature per key and a receiver accepts any of them.
9
+ // Spec: https://github.com/standard-webhooks/standard-webhooks/blob/main/spec/standard-webhooks.md
10
+ //
11
+ // Svix uses the same scheme, so OXYGEN's inbound verifiers (the `svix` library
12
+ // on the Clerk and Recall routes, and
13
+ // `apps/web/src/lib/ingress-capture/replay-verify.ts`) accept these signatures.
14
+ export const STANDARD_WEBHOOK_SECRET_PREFIX = "whsec_";
15
+ /** MCP Events accepts only `whsec_` secrets whose base64 value decodes to 24–64 bytes. */
16
+ export const STANDARD_WEBHOOK_SECRET_MIN_BYTES = 24;
17
+ export const STANDARD_WEBHOOK_SECRET_MAX_BYTES = 64;
18
+ /** Default `webhook-timestamp` window, in either direction, as in the reference libraries. */
19
+ export const STANDARD_WEBHOOK_TOLERANCE_SECONDS = 5 * 60;
20
+ /** A secret or signing input that cannot produce a valid signature. Never carries the secret. */
21
+ export class StandardWebhookError extends Error {
22
+ reason;
23
+ constructor(reason, message) {
24
+ super(message);
25
+ this.name = "StandardWebhookError";
26
+ this.reason = reason;
27
+ }
28
+ }
29
+ // Standard alphabet, padding optional. The reference decoders reject the
30
+ // URL-safe alphabet, and Buffer.from(value, "base64") silently skips stray
31
+ // characters, which would sign with a key the receiver never derives.
32
+ const STANDARD_BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}(?:==)?|[A-Za-z0-9+/]{3}=?)?$/;
33
+ // Printable ASCII without space or ".": the id is a header value and a
34
+ // "."-delimited part of the signed content, and the spec forbids "." in it.
35
+ const MESSAGE_ID = /^[\x21-\x2d\x2f-\x7e]+$/;
36
+ const UNIX_SECONDS = /^\d{1,15}$/;
37
+ /** Decode a `whsec_` secret to its HMAC key, enforcing the 24–64 byte range. */
38
+ export function parseStandardWebhookSecret(secret) {
39
+ if (!secret.startsWith(STANDARD_WEBHOOK_SECRET_PREFIX)) {
40
+ throw new StandardWebhookError("secret_prefix", "A Standard Webhooks secret must start with whsec_.");
41
+ }
42
+ const encoded = secret.slice(STANDARD_WEBHOOK_SECRET_PREFIX.length);
43
+ if (!STANDARD_BASE64.test(encoded)) {
44
+ throw new StandardWebhookError("secret_encoding", "A Standard Webhooks secret must be standard base64 after whsec_.");
45
+ }
46
+ const key = Buffer.from(encoded, "base64");
47
+ if (key.length < STANDARD_WEBHOOK_SECRET_MIN_BYTES || key.length > STANDARD_WEBHOOK_SECRET_MAX_BYTES) {
48
+ throw new StandardWebhookError("secret_length", `A Standard Webhooks secret must decode to ${STANDARD_WEBHOOK_SECRET_MIN_BYTES}-${STANDARD_WEBHOOK_SECRET_MAX_BYTES} bytes; this one decodes to ${key.length}.`);
49
+ }
50
+ return key;
51
+ }
52
+ /** The `webhook-signature` value: `v1,<base64>`, space-separated when several secrets sign. */
53
+ export function signStandardWebhook(input) {
54
+ const keys = parseSecrets(input.secret);
55
+ if (!MESSAGE_ID.test(input.id)) {
56
+ throw new StandardWebhookError("invalid_id", "A webhook id must be printable ASCII without spaces or full stops.");
57
+ }
58
+ const content = signedContent(input.id, requireUnixSeconds(input.timestamp), input.body);
59
+ return keys.map((key) => `v1,${hmacBase64(key, content)}`).join(" ");
60
+ }
61
+ /** The three Standard Webhooks headers for one delivery attempt. */
62
+ export function standardWebhookHeaders(input) {
63
+ const seconds = requireUnixSeconds(input.timestamp);
64
+ return {
65
+ "webhook-id": input.id,
66
+ "webhook-timestamp": String(seconds),
67
+ "webhook-signature": signStandardWebhook({ ...input, timestamp: seconds }),
68
+ };
69
+ }
70
+ /**
71
+ * True when any `v1` entry of `signature` matches any of the secrets and the
72
+ * timestamp is within `toleranceSeconds` of `now`. A malformed id, timestamp or
73
+ * signature is `false`; an invalid secret, `now` or `toleranceSeconds` throws,
74
+ * because that is a misconfiguration rather than a bad request.
75
+ */
76
+ export function verifyStandardWebhook(input) {
77
+ const keys = parseSecrets(input.secret);
78
+ const tolerance = input.toleranceSeconds ?? STANDARD_WEBHOOK_TOLERANCE_SECONDS;
79
+ if (!Number.isFinite(tolerance) || tolerance < 0) {
80
+ throw new RangeError("toleranceSeconds must be a finite, non-negative number of seconds.");
81
+ }
82
+ const now = input.now === undefined ? Math.floor(Date.now() / 1000) : requireUnixSeconds(input.now);
83
+ const seconds = readUnixSeconds(input.timestamp);
84
+ if (seconds === null || !MESSAGE_ID.test(input.id) || Math.abs(now - seconds) > tolerance)
85
+ return false;
86
+ const content = signedContent(input.id, seconds, input.body);
87
+ const expected = keys.map((key) => Buffer.from(hmacBase64(key, content)));
88
+ for (const entry of input.signature.split(" ")) {
89
+ const comma = entry.indexOf(",");
90
+ if (comma < 0 || entry.slice(0, comma) !== "v1")
91
+ continue;
92
+ const presented = Buffer.from(entry.slice(comma + 1));
93
+ if (expected.some((candidate) => candidate.length === presented.length && timingSafeEqual(candidate, presented))) {
94
+ return true;
95
+ }
96
+ }
97
+ return false;
98
+ }
99
+ function parseSecrets(secret) {
100
+ const secrets = typeof secret === "string" ? [secret] : secret;
101
+ if (secrets.length === 0) {
102
+ throw new StandardWebhookError("secret_missing", "At least one Standard Webhooks secret is required.");
103
+ }
104
+ return secrets.map(parseStandardWebhookSecret);
105
+ }
106
+ function signedContent(id, seconds, body) {
107
+ return `${id}.${seconds}.${body}`;
108
+ }
109
+ function hmacBase64(key, content) {
110
+ return createHmac("sha256", key).update(content, "utf8").digest("base64");
111
+ }
112
+ function requireUnixSeconds(value) {
113
+ const seconds = readUnixSeconds(value);
114
+ if (seconds === null) {
115
+ throw new StandardWebhookError("invalid_timestamp", "A webhook timestamp must be a valid time at or after the Unix epoch.");
116
+ }
117
+ return seconds;
118
+ }
119
+ function readUnixSeconds(value) {
120
+ if (typeof value === "string")
121
+ return UNIX_SECONDS.test(value) ? Number(value) : null;
122
+ const seconds = value instanceof Date ? Math.floor(value.getTime() / 1000) : value;
123
+ return Number.isSafeInteger(seconds) && seconds >= 0 ? seconds : null;
124
+ }
@@ -1,6 +1,6 @@
1
1
  import type { TagColor } from "./select-options.js";
2
2
  /** Primitive kinds that participate in the workspace tag union today. */
3
- export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project", "sender_profile", "voice_number"];
3
+ export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record", "domain", "project", "sender_profile", "voice_number", "signal_type", "dashboard", "agent", "ad_search_collection"];
4
4
  export type TagKind = (typeof TAG_KINDS)[number];
5
5
  /**
6
6
  * The taggable kinds in prose, for CLI `--help` and MCP tool descriptions.
@@ -26,6 +26,10 @@ export const TAG_KINDS = [
26
26
  "project",
27
27
  "sender_profile",
28
28
  "voice_number",
29
+ "signal_type",
30
+ "dashboard",
31
+ "agent",
32
+ "ad_search_collection",
29
33
  ];
30
34
  /**
31
35
  * The taggable kinds in prose, for CLI `--help` and MCP tool descriptions.
@@ -39,7 +43,7 @@ export const TAG_KINDS = [
39
43
  */
40
44
  export const TAG_KINDS_PROSE = "publishing posts, knowledge pages, sequences, tables, workflows, recipes, " +
41
45
  "inbox conversations, CRM records, mailboxes, sender accounts, sending domains, " +
42
- "projects, sender profiles, and phone numbers";
46
+ "projects, sender profiles, phone numbers, signal types, dashboards, agents, and saved Ads searches";
43
47
  /**
44
48
  * The phrase each kind must contribute to {@link TAG_KINDS_PROSE}. Typed as a
45
49
  * total map over TagKind, so adding a kind is a compile error until it is named
@@ -60,6 +64,10 @@ export const TAG_KIND_PROSE_MARKERS = {
60
64
  project: "project",
61
65
  sender_profile: "sender profile",
62
66
  voice_number: "phone number",
67
+ signal_type: "signal type",
68
+ dashboard: "dashboard",
69
+ agent: "agent",
70
+ ad_search_collection: "saved Ads search",
63
71
  };
64
72
  export function isTagKind(value) {
65
73
  return typeof value === "string" && TAG_KINDS.includes(value);
@@ -91,6 +99,10 @@ export const TAG_KIND_LABELS = {
91
99
  project: { one: "project", many: "projects" },
92
100
  sender_profile: { one: "sender profile", many: "sender profiles" },
93
101
  voice_number: { one: "phone number", many: "phone numbers" },
102
+ signal_type: { one: "signal type", many: "signal types" },
103
+ dashboard: { one: "dashboard", many: "dashboards" },
104
+ agent: { one: "agent", many: "agents" },
105
+ ad_search_collection: { one: "saved Ads search", many: "saved Ads searches" },
94
106
  };
95
107
  /** Inline label for a count: `tagKindLabel("sequence", 1)` → "sequence". */
96
108
  export function tagKindLabel(kind, count) {
@@ -8,8 +8,12 @@ export type UgcPostingTarget = {
8
8
  };
9
9
  export declare const UGC_POSTING_TARGET_MAX = 100;
10
10
  export declare const UGC_POSTING_TARGET_PERIODS: readonly ["week", "month", "total"];
11
- /** The operator-written creator brief is short by design (Philipp, 2026-09-23). */
12
- export declare const UGC_PROGRAM_BRIEF_MAX_CHARS = 1200;
11
+ /**
12
+ * The operator-written creator brief: one page, not a document (Philipp, 2026-09-23;
13
+ * raised from 1,200 on 2026-09-29, when a natural founder brief ran 1,298 and blind
14
+ * operators either trimmed it or left the program without one).
15
+ */
16
+ export declare const UGC_PROGRAM_BRIEF_MAX_CHARS = 4000;
13
17
  /** The program's target as its API shape, or null when none is set. */
14
18
  export declare function ugcPostingTarget(program: {
15
19
  postingTargetCount?: number | null | undefined;
@@ -21,7 +25,6 @@ export type UgcProgram = {
21
25
  productUrl: string;
22
26
  description: string;
23
27
  knowledgePageSlugs: string[];
24
- brandApprovalRequired: boolean;
25
28
  peerEngagementEnabled?: boolean;
26
29
  peerEngagementVersion?: number;
27
30
  peerEngagementEffectiveAt?: Date | null;
@@ -94,13 +97,11 @@ export type UgcPostLink = {
94
97
  participationId: string;
95
98
  creatorOrgId: string;
96
99
  masterOrgId: string;
97
- brandApprovalRequired: boolean;
98
100
  sourcePostId: string;
99
101
  attribution: Record<string, unknown>;
100
102
  projection: UgcPostProjection;
101
103
  sourceRevision: string;
102
104
  creatorApprovedRevision: string | null;
103
- brandApprovedRevision: string | null;
104
105
  grantVersion: number;
105
106
  removedAt?: Date | null;
106
107
  enrollmentVersion?: number;
@@ -154,13 +155,16 @@ export declare const UGC_SPONSORED_LINKEDIN_SEATS = 1;
154
155
  * One label for a sponsor's row (`view=sponsorship`) on every surface, the web
155
156
  * Creators tab and the MCP widget alike: a card sponsorship waiting on its
156
157
  * checkout reads differently from one waiting on the provider, and a stopped
157
- * renewal says so while its paid period runs.
158
+ * renewal says so while its paid period runs. `paid_through` tells a stop that
159
+ * keeps a paid period from one that ended it, and from a sponsorship that was
160
+ * cancelled before anything was paid.
158
161
  */
159
162
  export declare function ugcSponsorshipStatusLabel(row: {
160
163
  status: string;
161
164
  checkout_url?: string | null;
162
165
  cancellation_pending?: boolean;
163
- }): string;
166
+ paid_through?: string | null;
167
+ }, now?: number): string;
164
168
  /** What confirming a credit-sponsorship quote does for one creator (`participants[].state`). */
165
169
  export declare const UGC_SPONSORSHIP_PLAN_LABELS: Readonly<Record<string, string>>;
166
170
  /** When a new credit sponsorship's reservation starts (`participants[].reservation_starts`). */
@@ -1,7 +1,11 @@
1
1
  export const UGC_POSTING_TARGET_MAX = 100;
2
2
  export const UGC_POSTING_TARGET_PERIODS = ["week", "month", "total"];
3
- /** The operator-written creator brief is short by design (Philipp, 2026-09-23). */
4
- export const UGC_PROGRAM_BRIEF_MAX_CHARS = 1200;
3
+ /**
4
+ * The operator-written creator brief: one page, not a document (Philipp, 2026-09-23;
5
+ * raised from 1,200 on 2026-09-29, when a natural founder brief ran 1,298 and blind
6
+ * operators either trimmed it or left the program without one).
7
+ */
8
+ export const UGC_PROGRAM_BRIEF_MAX_CHARS = 4000;
5
9
  /** The program's target as its API shape, or null when none is set. */
6
10
  export function ugcPostingTarget(program) {
7
11
  return program.postingTargetCount && program.postingTargetPeriod
@@ -14,17 +18,21 @@ export const UGC_SPONSORED_LINKEDIN_SEATS = 1;
14
18
  * One label for a sponsor's row (`view=sponsorship`) on every surface, the web
15
19
  * Creators tab and the MCP widget alike: a card sponsorship waiting on its
16
20
  * checkout reads differently from one waiting on the provider, and a stopped
17
- * renewal says so while its paid period runs.
21
+ * renewal says so while its paid period runs. `paid_through` tells a stop that
22
+ * keeps a paid period from one that ended it, and from a sponsorship that was
23
+ * cancelled before anything was paid.
18
24
  */
19
- export function ugcSponsorshipStatusLabel(row) {
25
+ export function ugcSponsorshipStatusLabel(row, now = Date.now()) {
26
+ const paidThrough = row.paid_through ? Date.parse(row.paid_through) : Number.NaN;
27
+ const paid = Number.isFinite(paidThrough);
20
28
  if (row.cancellation_pending)
21
- return "Renewal stopping";
29
+ return paid ? "Renewal stopping" : "Cancelling";
22
30
  if (row.status === "active")
23
31
  return "Sponsored";
24
32
  if (row.status === "pending")
25
33
  return row.checkout_url ? "Checkout open" : "Pending";
26
34
  if (row.status === "cancelled")
27
- return "Renewal stopped";
35
+ return !paid ? "Not sponsored" : paidThrough > now ? "Renewal stopped" : "Ended";
28
36
  return "Not sponsored";
29
37
  }
30
38
  /** What confirming a credit-sponsorship quote does for one creator (`participants[].state`). */
@@ -1 +1 @@
1
- export declare const OXYGEN_BUILD_VERSION = "1.1011.98";
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1011.322";
@@ -1,2 +1,2 @@
1
1
  // GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
2
- export const OXYGEN_BUILD_VERSION = "1.1011.98";
2
+ export const OXYGEN_BUILD_VERSION = "1.1011.322";
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Wall-clock time in an IANA time zone, converted to instants DST-correctly.
3
+ *
4
+ * Publishing's publish-time contract rides on this on every surface: an ISO
5
+ * date-time with `Z` or an offset is that exact instant, and one without is
6
+ * wall-clock time in the post's timezone, read with the offset in force at that
7
+ * local time. 09:00 Europe/Berlin is 07:00Z in October and 08:00Z in December.
8
+ */
9
+ /**
10
+ * Is `zone` an IANA time zone this runtime resolves (`Europe/Berlin`, `UTC`)?
11
+ * A bare offset (`+02:00`) is refused although Intl accepts it: it never moves
12
+ * for daylight saving, so a schedule kept in it drifts an hour for half the year.
13
+ */
14
+ export declare function isValidTimeZone(zone: string): boolean;
15
+ /** `timeZone`'s offset from UTC at `instant`, in milliseconds (Berlin in July: 7,200,000). */
16
+ export declare function zoneOffsetMs(instant: Date, timeZone: string): number;
17
+ export type WallClock = {
18
+ year: number;
19
+ /** 1-12. */
20
+ month: number;
21
+ day: number;
22
+ hour: number;
23
+ minute: number;
24
+ second: number;
25
+ millisecond: number;
26
+ };
27
+ /**
28
+ * The instant a wall-clock time in `timeZone` names. A time the zone repeats (the
29
+ * autumn fall-back hour) resolves to its first occurrence; a time it skips (the
30
+ * spring-forward gap) resolves past the gap by its length, as a clock would read
31
+ * it: 02:30 Europe/Berlin on 2026-03-29 is 03:30 CEST.
32
+ */
33
+ export declare function zonedWallClockToInstant(wall: WallClock, timeZone: string): Date;
34
+ /**
35
+ * Parse an ISO date-time: with `Z` or an offset it is that instant; without one
36
+ * it is wall-clock time in `timeZone` (DST-correct). Null when it names no real
37
+ * date-time. Anything that is not ISO-shaped keeps `Date`'s own parsing.
38
+ */
39
+ export declare function parseDateTimeInZone(value: string, timeZone: string): Date | null;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Wall-clock time in an IANA time zone, converted to instants DST-correctly.
3
+ *
4
+ * Publishing's publish-time contract rides on this on every surface: an ISO
5
+ * date-time with `Z` or an offset is that exact instant, and one without is
6
+ * wall-clock time in the post's timezone, read with the offset in force at that
7
+ * local time. 09:00 Europe/Berlin is 07:00Z in October and 08:00Z in December.
8
+ */
9
+ const DAY_MS = 24 * 60 * 60 * 1000;
10
+ /**
11
+ * Is `zone` an IANA time zone this runtime resolves (`Europe/Berlin`, `UTC`)?
12
+ * A bare offset (`+02:00`) is refused although Intl accepts it: it never moves
13
+ * for daylight saving, so a schedule kept in it drifts an hour for half the year.
14
+ */
15
+ export function isValidTimeZone(zone) {
16
+ const value = zone.trim();
17
+ if (!value || /^[+-]/.test(value))
18
+ return false;
19
+ try {
20
+ new Intl.DateTimeFormat("en-US", { timeZone: value });
21
+ return true;
22
+ }
23
+ catch {
24
+ return false;
25
+ }
26
+ }
27
+ /** `timeZone`'s offset from UTC at `instant`, in milliseconds (Berlin in July: 7,200,000). */
28
+ export function zoneOffsetMs(instant, timeZone) {
29
+ const parts = new Intl.DateTimeFormat("en-US", {
30
+ timeZone,
31
+ hourCycle: "h23",
32
+ year: "numeric",
33
+ month: "2-digit",
34
+ day: "2-digit",
35
+ hour: "2-digit",
36
+ minute: "2-digit",
37
+ second: "2-digit",
38
+ }).formatToParts(instant);
39
+ const read = (type) => Number(parts.find((part) => part.type === type)?.value ?? 0);
40
+ const asUtc = Date.UTC(read("year"), read("month") - 1, read("day"), read("hour"), read("minute"), read("second"));
41
+ return asUtc - Math.floor(instant.getTime() / 1000) * 1000;
42
+ }
43
+ /**
44
+ * The instant a wall-clock time in `timeZone` names. A time the zone repeats (the
45
+ * autumn fall-back hour) resolves to its first occurrence; a time it skips (the
46
+ * spring-forward gap) resolves past the gap by its length, as a clock would read
47
+ * it: 02:30 Europe/Berlin on 2026-03-29 is 03:30 CEST.
48
+ */
49
+ export function zonedWallClockToInstant(wall, timeZone) {
50
+ const asUtc = Date.UTC(wall.year, wall.month - 1, wall.day, wall.hour, wall.minute, wall.second, wall.millisecond);
51
+ const before = zoneOffsetMs(new Date(asUtc - DAY_MS), timeZone);
52
+ const after = zoneOffsetMs(new Date(asUtc + DAY_MS), timeZone);
53
+ // Each offset in force around that day names one candidate; a candidate is real
54
+ // when the zone maps it back to this same wall-clock time.
55
+ const real = [...new Set([before, after])]
56
+ .map((offset) => asUtc - offset)
57
+ .filter((instant) => zoneOffsetMs(new Date(instant), timeZone) === asUtc - instant)
58
+ .sort((left, right) => left - right);
59
+ return new Date(real[0] ?? asUtc - before);
60
+ }
61
+ // YYYY-MM-DD, optionally THH:MM[:SS[.fff]], and nothing after it: no Z, no offset.
62
+ const LOCAL_ISO_DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})(?:[T ](\d{2}):(\d{2})(?::(\d{2})(?:[.,](\d{1,9}))?)?)?$/;
63
+ /**
64
+ * Parse an ISO date-time: with `Z` or an offset it is that instant; without one
65
+ * it is wall-clock time in `timeZone` (DST-correct). Null when it names no real
66
+ * date-time. Anything that is not ISO-shaped keeps `Date`'s own parsing.
67
+ */
68
+ export function parseDateTimeInZone(value, timeZone) {
69
+ const text = value.trim();
70
+ const local = LOCAL_ISO_DATE_TIME.exec(text);
71
+ if (!local) {
72
+ const instant = new Date(text);
73
+ return Number.isFinite(instant.getTime()) ? instant : null;
74
+ }
75
+ const [year, month, day, hour, minute, second] = local.slice(1, 7).map((part) => Number(part ?? 0));
76
+ const millisecond = local[7] ? Math.floor(Number(`0.${local[7]}`) * 1000) : 0;
77
+ const daysInMonth = new Date(Date.UTC(year, month, 0)).getUTCDate();
78
+ if (month < 1 || month > 12 || day < 1 || day > daysInMonth || hour > 23 || minute > 59 || second > 59)
79
+ return null;
80
+ return zonedWallClockToInstant({ year, month, day, hour, minute, second, millisecond }, timeZone);
81
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.1011.98",
3
+ "version": "1.1011.322",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",