@oxygen-agent/cli 1.1011.493 → 1.1011.607

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 (72) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +7 -0
  3. package/dist/crm-file-read-commands.d.ts +6 -0
  4. package/dist/crm-file-read-commands.js +16 -0
  5. package/dist/feed-read-flags.d.ts +3 -0
  6. package/dist/feed-read-flags.js +9 -0
  7. package/dist/functions-commands.js +2 -1
  8. package/dist/help.js +18 -5
  9. package/dist/index.js +168 -107
  10. package/dist/sender-limits-flags.js +6 -4
  11. package/dist/settings-commands.js +11 -11
  12. package/dist/whatsapp-account-flags.d.ts +4 -0
  13. package/dist/whatsapp-account-flags.js +18 -0
  14. package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
  15. package/node_modules/@oxygen/shared/dist/billing.d.ts +41 -0
  16. package/node_modules/@oxygen/shared/dist/billing.js +56 -0
  17. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +21 -0
  18. package/node_modules/@oxygen/shared/dist/capability-discovery.js +254 -21
  19. package/node_modules/@oxygen/shared/dist/contracts/billing-owner-read.d.ts +6 -0
  20. package/node_modules/@oxygen/shared/dist/contracts/billing-owner-read.js +7 -0
  21. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
  22. package/node_modules/@oxygen/shared/dist/contracts/copilot-sessions.d.ts +11 -0
  23. package/node_modules/@oxygen/shared/dist/contracts/copilot-sessions.js +7 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/crm-file-bytes.d.ts +33 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/crm-file-bytes.js +12 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +6 -3
  27. package/node_modules/@oxygen/shared/dist/contracts/function-library-read.d.ts +14 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/function-library-read.js +16 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/inbox-drafts-read.d.ts +45 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/inbox-drafts-read.js +21 -0
  31. package/node_modules/@oxygen/shared/dist/contracts/limits-read.d.ts +29 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +121 -0
  33. package/node_modules/@oxygen/shared/dist/contracts/linkedin-invites-list.js +1 -1
  34. package/node_modules/@oxygen/shared/dist/contracts/pipedrive-catalog.d.ts +18 -0
  35. package/node_modules/@oxygen/shared/dist/contracts/pipedrive-catalog.js +29 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.d.ts +181 -0
  37. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +62 -0
  38. package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +7 -5
  39. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.d.ts +3 -0
  40. package/node_modules/@oxygen/shared/dist/contracts/sequence-list.js +8 -0
  41. package/node_modules/@oxygen/shared/dist/contracts/stored-read-window.d.ts +17 -0
  42. package/node_modules/@oxygen/shared/dist/contracts/stored-read-window.js +8 -0
  43. package/node_modules/@oxygen/shared/dist/contracts/table-feed-read.d.ts +67 -0
  44. package/node_modules/@oxygen/shared/dist/contracts/table-feed-read.js +19 -0
  45. package/node_modules/@oxygen/shared/dist/contracts/table-row-read.d.ts +30 -0
  46. package/node_modules/@oxygen/shared/dist/contracts/table-row-read.js +9 -0
  47. package/node_modules/@oxygen/shared/dist/contracts/whatsapp-reads.d.ts +111 -0
  48. package/node_modules/@oxygen/shared/dist/contracts/whatsapp-reads.js +33 -0
  49. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.d.ts +6 -5
  50. package/node_modules/@oxygen/shared/dist/contracts/workspace-pages.js +5 -5
  51. package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +4 -1
  52. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +17 -0
  53. package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +26 -0
  54. package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +2 -32
  55. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +5 -0
  56. package/node_modules/@oxygen/shared/dist/feature-gates.js +6 -0
  57. package/node_modules/@oxygen/shared/dist/first-month-offer-window.d.ts +34 -0
  58. package/node_modules/@oxygen/shared/dist/first-month-offer-window.js +41 -0
  59. package/node_modules/@oxygen/shared/dist/first-month-offer.d.ts +42 -0
  60. package/node_modules/@oxygen/shared/dist/first-month-offer.js +56 -0
  61. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +32 -1
  62. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +68 -4
  63. package/node_modules/@oxygen/shared/dist/otlp-log-sink.d.ts +6 -2
  64. package/node_modules/@oxygen/shared/dist/otlp-log-sink.js +6 -1
  65. package/node_modules/@oxygen/shared/dist/sql-error.js +13 -2
  66. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.d.ts +3 -0
  67. package/node_modules/@oxygen/shared/dist/stripe-price-catalog.js +17 -1
  68. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  69. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  70. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +11 -1
  71. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +29 -3
  72. package/package.json +1 -1
@@ -4,7 +4,7 @@
4
4
  * the route accepts; the route's own validators (working-hours rules, warm-up
5
5
  * date parsing) still run after the contract.
6
6
  */
7
- import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_KEYS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, } from "../linkedin-sequences.js";
7
+ import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_KEYS, LINKEDIN_SENDER_LIMIT_MAXIMUMS, linkedInSenderLimitCautionLevel, } from "../linkedin-sequences.js";
8
8
  import { arr, bool, enumOf, int, nullable, obj, str } from "./schema.js";
9
9
  /** Short MCP-facing notes; the CLI carries its own longer help per flag. */
10
10
  const LIMIT_NOTES = {
@@ -18,6 +18,7 @@ const LIMIT_NOTES = {
18
18
  sales_nav_search_results_per_day: "Result rows.",
19
19
  api_reads_per_day: "Other API reads.",
20
20
  total_reads_per_day: "All reads.",
21
+ min_action_spacing_seconds: "Floor; campaigns spread each day's limit over working hours (20 invites in 07:00-22:00: 30-45 min apart).",
21
22
  interactive_min_spacing_seconds: "Human Unibox send spacing.",
22
23
  interactive_spacing_jitter_seconds: "Jitter on that spacing.",
23
24
  interactive_sends_per_hour: "Hourly human send cap.",
@@ -30,11 +31,12 @@ const LIMIT_NOTES = {
30
31
  function limit(key) {
31
32
  const max = LINKEDIN_SENDER_LIMIT_MAXIMUMS[key];
32
33
  const note = LIMIT_NOTES[key];
34
+ const caution = linkedInSenderLimitCautionLevel(key);
33
35
  return int({
34
36
  minimum: 0,
35
37
  maximum: max,
36
38
  clamp: true,
37
- description: `Max ${max} (default ${LINKEDIN_SENDER_LIMIT_DEFAULTS[key]}).${note ? ` ${note}` : ""}`,
39
+ description: `Max ${max} (default ${LINKEDIN_SENDER_LIMIT_DEFAULTS[key]}).${note ? ` ${note}` : ""}${caution === undefined ? "" : ` Above ${caution} raises the risk of a LinkedIn restriction.`}`,
38
40
  });
39
41
  }
40
42
  export const SENDER_LIMITS_CONTRACT = obj(Object.fromEntries(LINKEDIN_SENDER_LIMIT_KEYS.map((key) => [key, limit(key)])), { description: "Per-account caps for sends and reads. Values above the maximum are clamped server-side." });
@@ -44,7 +46,7 @@ export const SENDER_WORKING_HOURS_CONTRACT = obj({
44
46
  days: arr(int({ minimum: 1, maximum: 7 }), { description: "ISO weekdays, 1=Mon." }),
45
47
  start: str({ description: "HH:MM, e.g. 09:00." }),
46
48
  end: str({ description: "HH:MM, after start." }),
47
- }, { description: "When LinkedIn actions may run (default 07:00-22:00 daily); work outside waits. Omitted fields keep their value." });
49
+ }, { description: "When LinkedIn actions may run (default 07:00-22:00 daily); campaigns spread each day's limit across them, starting at a different time each day. Work outside waits. Omitted fields keep their value." });
48
50
  export const SENDER_WARMUP_CONTRACT = obj({
49
51
  enabled: bool({ description: "true = start/restart the ramp at day 0; false = disable warm-up (account becomes warm)." }),
50
52
  start_date: nullable(str({
@@ -74,13 +76,13 @@ export const SENDER_LIMITS_SET_OPERATION = {
74
76
  contract: SENDER_LIMITS_PATCH_CONTRACT,
75
77
  cli: {
76
78
  path: ["senders", "limits", "set"],
77
- summary: "Adjust per-account action limits and working hours. LinkedIn actions only run inside the account's working hours (default 07:00-22:00 every day, in the account's timezone); work due outside them is deferred to the next window, not failed. Set them with --timezone, --working-days, --working-hours-start and --working-hours-end; flags you omit keep their current value. A sequence's sending schedule (sequences update --schedule-*, or --linkedin-* for LinkedIn alone) can narrow the window for its LinkedIn steps, never widen it; email steps follow the sequence's email send window instead. Every limit flag shows its default and hard maximum; values above the maximum are clamped, and higher caps raise the risk of a LinkedIn restriction. Each action has its own cap (InMails per day and per rolling 30 days, profile views, profile lookups, endorsements, invitation withdrawals, posts), and every send except posts also counts toward --total-actions-per-day. Daily send caps run at half on Saturday and Sunday in the account's timezone unless --weekend-full-volume is set. A withdrawn invitation cannot be re-sent to the same person for 21 days. Own-feed posts and your own Unibox replies ignore working hours. A refused action names its reason (outside_working_hours defers it; linkedin_rate_limited names the limit and resets_at). Changing limits is free. <id> accepts a sender account id, connection id, or Unipile account id.",
79
+ summary: "Adjust per-account action limits and working hours. LinkedIn actions only run inside the account's working hours (default 07:00-22:00 every day, in the account's timezone); work due outside them is deferred to the next window, not failed. Set them with --timezone, --working-days, --working-hours-start and --working-hours-end; flags you omit keep their current value. A sequence's sending schedule (sequences update --schedule-*, or --linkedin-* for LinkedIn alone) can narrow the window for its LinkedIn steps, never widen it; email steps follow the sequence's email send window instead. Every limit flag shows its default and hard maximum; values above the maximum are clamped, and higher caps raise the risk of a LinkedIn restriction. Each action has its own cap (InMails per day and per rolling 30 days, profile views, profile lookups, endorsements, invitation withdrawals, posts), and every send except posts also counts toward --total-actions-per-day. Daily send caps run at half on Saturday and Sunday in the account's timezone unless --weekend-full-volume is set. A withdrawn invitation cannot be re-sent to the same person for 21 days. Own-feed posts and your own Unibox replies ignore working hours. A refused action names its reason (outside_working_hours defers it; linkedin_rate_limited names the limit and resets_at). Caps saved before 25 Sep 2026 above 40 messages, 20 comments or 200 total actions were lowered to those levels once on 2 Oct 2026 (old values in sender metadata linkedin_limit_caution_2026_10); limits you save now are kept as set. Raising caps to answer a post's commenters: read `oxygen skills get oxygen-linkedin-marketing --file guides/safe-limits-doctrine.md` first. Changing limits is free. <id> accepts a sender account id, connection id, or Unipile account id.",
78
80
  argument: { name: "id", description: "Sender account id, connection id, or Unipile account id." },
79
81
  },
80
82
  mcp: {
81
83
  name: "oxygen_senders_limits_set",
82
84
  title: "Oxygen: Set LinkedIn Account Limits",
83
- description: "Updates per-account daily limits for sends (invites/messages/InMails) AND reads (relations lists, message history, searches, profile views), working hours, and the warm-up ramp (enable/disable/anchor). Values are clamped to safe maximums to protect the LinkedIn account. LinkedIn actions run only inside working hours.",
85
+ description: "Updates per-account daily limits for sends (invites/messages/InMails) AND reads (relations lists, message history, searches, profile views), working hours, and the warm-up ramp (enable/disable/anchor). Values are clamped to hard maximums; above the caution levels restriction risk rises. LinkedIn actions run only inside working hours.",
84
86
  pack: "deliverability",
85
87
  },
86
88
  mutates: true,
@@ -0,0 +1,3 @@
1
+ export declare const SEQUENCE_LIST_PROJECTION_CONTRACT: import("./schema.js").ContractObject<{
2
+ readonly omit: import("./schema.js").StringProperty;
3
+ }, never>;
@@ -0,0 +1,8 @@
1
+ /** The optional warning projection for GET /api/cli/sequences.
2
+ * Other list filters retain their existing contracts. Unknown CSV values are
3
+ * ignored by the API; absence retains the normal warning reads and results.
4
+ */
5
+ import { obj, str } from "./schema.js";
6
+ export const SEQUENCE_LIST_PROJECTION_CONTRACT = obj({
7
+ omit: str({ description: "warnings: skip warnings and their extra reads." }),
8
+ });
@@ -0,0 +1,17 @@
1
+ /** Optional representations of an already-authorized stored native read. */
2
+ export declare const STORED_READ_WINDOW_FIELDS: {
3
+ readonly projection: {
4
+ type: "string";
5
+ enum: readonly ("full" | "summary" | "value")[];
6
+ description?: string;
7
+ };
8
+ readonly path: import("./schema.js").StringProperty;
9
+ readonly value_cursor: import("./schema.js").StringProperty;
10
+ readonly max_chars: {
11
+ type: "integer";
12
+ description?: string;
13
+ minimum?: number;
14
+ maximum?: number;
15
+ "x-clamp"?: true;
16
+ };
17
+ };
@@ -0,0 +1,8 @@
1
+ import { enumOf, int, str } from "./schema.js";
2
+ /** Optional representations of an already-authorized stored native read. */
3
+ export const STORED_READ_WINDOW_FIELDS = {
4
+ projection: enumOf(["summary", "full", "value"], { description: "summary: metadata without long values; full: complete rows; value: lossless JSON fragments for one exact selection. Omit for legacy output." }),
5
+ path: str({ maxLength: 1024, description: "value: JSON Pointer into the public native result; empty selects the whole result." }),
6
+ value_cursor: str({ minLength: 1, maxLength: 2000, description: "value: previous next_value_cursor with identical selection and path. Changed source refuses continuation." }),
7
+ max_chars: int({ minimum: 1000, maximum: 40000, description: "Opt-in complete receipt limit. MCP caps external at 10000, built-in at 4000. Oversize values remain accessible with value windows." }),
8
+ };
@@ -0,0 +1,67 @@
1
+ import { type Infer } from "./schema.js";
2
+ export declare const TABLE_FEED_LIST_MAX_LIMIT = 200;
3
+ export declare const TABLE_FEED_LIST_DEFAULT_LIMIT = 50;
4
+ export declare const TABLE_FEED_LIST_LIMIT_PROPERTY: {
5
+ type: "integer";
6
+ description?: string;
7
+ minimum?: number;
8
+ maximum?: number;
9
+ "x-clamp"?: true;
10
+ };
11
+ /** Representation controls only; existing feed selection/coercion remains native. */
12
+ export declare const TABLE_FEED_LIST_WINDOW_CONTRACT: import("./schema.js").ContractObject<{
13
+ readonly projection: {
14
+ readonly type: "string";
15
+ readonly enum: readonly ["summary", "full"];
16
+ readonly description: "summary: metadata without long settings; full: complete feeds. Omit for legacy output.";
17
+ };
18
+ readonly offset: {
19
+ type: "integer";
20
+ description?: string;
21
+ minimum?: number;
22
+ maximum?: number;
23
+ "x-clamp"?: true;
24
+ };
25
+ readonly max_chars: {
26
+ type: "integer";
27
+ description?: string;
28
+ minimum?: number;
29
+ maximum?: number;
30
+ "x-clamp"?: true;
31
+ };
32
+ }, never>;
33
+ export declare const TABLE_FEED_GET_WINDOW_CONTRACT: import("./schema.js").ContractObject<{
34
+ readonly projection: {
35
+ type: "string";
36
+ enum: readonly ("full" | "summary" | "value")[];
37
+ description?: string;
38
+ };
39
+ readonly path: import("./schema.js").StringProperty;
40
+ readonly value_cursor: import("./schema.js").StringProperty;
41
+ readonly max_chars: {
42
+ type: "integer";
43
+ description?: string;
44
+ minimum?: number;
45
+ maximum?: number;
46
+ "x-clamp"?: true;
47
+ };
48
+ }, never>;
49
+ export type TableFeedListWindow = Infer<typeof TABLE_FEED_LIST_WINDOW_CONTRACT>;
50
+ export type TableFeedGetWindow = Infer<typeof TABLE_FEED_GET_WINDOW_CONTRACT>;
51
+ /** MCP keeps executable constraints; guidance appears once on each owner. */
52
+ export declare function tableFeedWindowProperties(contract: typeof TABLE_FEED_GET_WINDOW_CONTRACT | typeof TABLE_FEED_LIST_WINDOW_CONTRACT): {
53
+ [k: string]: {
54
+ type: "string";
55
+ enum?: readonly string[];
56
+ minLength?: number;
57
+ maxLength?: number;
58
+ } | {
59
+ type: "integer";
60
+ minimum?: number;
61
+ maximum?: number;
62
+ "x-clamp"?: true;
63
+ } | {
64
+ type: "string";
65
+ enum: readonly ("full" | "summary" | "value")[];
66
+ };
67
+ };
@@ -0,0 +1,19 @@
1
+ import { int, obj } from "./schema.js";
2
+ import { STORED_READ_WINDOW_FIELDS } from "./stored-read-window.js";
3
+ export const TABLE_FEED_LIST_MAX_LIMIT = 200;
4
+ export const TABLE_FEED_LIST_DEFAULT_LIMIT = 50;
5
+ export const TABLE_FEED_LIST_LIMIT_PROPERTY = int({ minimum: 1, maximum: TABLE_FEED_LIST_MAX_LIMIT, description: "Maximum feeds. Defaults to 50." });
6
+ /** Representation controls only; existing feed selection/coercion remains native. */
7
+ export const TABLE_FEED_LIST_WINDOW_CONTRACT = obj({
8
+ projection: { ...STORED_READ_WINDOW_FIELDS.projection, enum: ["summary", "full"], description: "summary: metadata without long settings; full: complete feeds. Omit for legacy output." },
9
+ offset: int({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER - 200, description: "Live ordered queue offset; resume with next_offset and identical filters. No snapshot/global total is claimed." }),
10
+ max_chars: STORED_READ_WINDOW_FIELDS.max_chars,
11
+ });
12
+ export const TABLE_FEED_GET_WINDOW_CONTRACT = obj(STORED_READ_WINDOW_FIELDS);
13
+ /** MCP keeps executable constraints; guidance appears once on each owner. */
14
+ export function tableFeedWindowProperties(contract) {
15
+ return Object.fromEntries(Object.entries(contract.properties).map(([key, property]) => {
16
+ const { description: _description, ...schema } = property;
17
+ return [key, schema];
18
+ }));
19
+ }
@@ -0,0 +1,30 @@
1
+ /** Representation controls for the existing exact-row read. Constraints stay
2
+ * shared with the native signer; each hint appears once in the selected schema. */
3
+ export declare const TABLE_ROW_READ_WINDOW_FIELDS: {
4
+ readonly projection: {
5
+ readonly type: "string";
6
+ readonly enum: readonly ["full", "value"];
7
+ readonly description: "value: lossless JSON fragments; full: complete result. Omit for legacy.";
8
+ };
9
+ readonly path: {
10
+ readonly type: "string";
11
+ readonly enum?: readonly string[];
12
+ readonly minLength?: number;
13
+ readonly maxLength?: number;
14
+ readonly description: "JSON Pointer; empty=root.";
15
+ };
16
+ readonly value_cursor: {
17
+ readonly type: "string";
18
+ readonly enum?: readonly string[];
19
+ readonly minLength?: number;
20
+ readonly maxLength?: number;
21
+ readonly description: "Resume with identical selection/path.";
22
+ };
23
+ readonly max_chars: {
24
+ readonly type: "integer";
25
+ readonly minimum?: number;
26
+ readonly maximum?: number;
27
+ readonly "x-clamp"?: true;
28
+ readonly description: "Receipt cap; MCP 10000, built-in 4000.";
29
+ };
30
+ };
@@ -0,0 +1,9 @@
1
+ import { STORED_READ_WINDOW_FIELDS } from "./stored-read-window.js";
2
+ /** Representation controls for the existing exact-row read. Constraints stay
3
+ * shared with the native signer; each hint appears once in the selected schema. */
4
+ export const TABLE_ROW_READ_WINDOW_FIELDS = {
5
+ projection: { ...STORED_READ_WINDOW_FIELDS.projection, enum: ["full", "value"], description: "value: lossless JSON fragments; full: complete result. Omit for legacy." },
6
+ path: { ...STORED_READ_WINDOW_FIELDS.path, description: "JSON Pointer; empty=root." },
7
+ value_cursor: { ...STORED_READ_WINDOW_FIELDS.value_cursor, description: "Resume with identical selection/path." },
8
+ max_chars: { ...STORED_READ_WINDOW_FIELDS.max_chars, description: "Receipt cap; MCP 10000, built-in 4000." },
9
+ };
@@ -0,0 +1,111 @@
1
+ export declare const WHATSAPP_ACCOUNT_STATUSES: readonly ["active", "paused", "disconnected", "restricted", "credentials_required"];
2
+ export declare const WHATSAPP_ACCOUNTS_READ_CONTRACT: import("./schema.js").ContractObject<{
3
+ readonly projection: {
4
+ type: "string";
5
+ enum: readonly ("full" | "summary" | "value")[];
6
+ description?: string;
7
+ };
8
+ readonly path: import("./schema.js").StringProperty;
9
+ readonly value_cursor: import("./schema.js").StringProperty;
10
+ readonly max_chars: {
11
+ type: "integer";
12
+ description?: string;
13
+ minimum?: number;
14
+ maximum?: number;
15
+ "x-clamp"?: true;
16
+ };
17
+ readonly status: {
18
+ type: "string";
19
+ enum: readonly ("active" | "credentials_required" | "disconnected" | "paused" | "restricted")[];
20
+ description?: string;
21
+ };
22
+ readonly include_usage: import("./schema.js").BooleanProperty;
23
+ readonly include_disconnected: import("./schema.js").BooleanProperty;
24
+ readonly tag: import("./schema.js").StringProperty;
25
+ readonly account_id: import("./schema.js").StringProperty;
26
+ readonly limit: {
27
+ type: "integer";
28
+ description?: string;
29
+ minimum?: number;
30
+ maximum?: number;
31
+ "x-clamp"?: true;
32
+ };
33
+ readonly offset: {
34
+ type: "integer";
35
+ description?: string;
36
+ minimum?: number;
37
+ maximum?: number;
38
+ "x-clamp"?: true;
39
+ };
40
+ }, never>;
41
+ export declare const WHATSAPP_DRAFT_STATUSES: readonly ["queued", "edited", "approved", "sent", "rejected"];
42
+ export declare const WHATSAPP_DRAFTS_READ_CONTRACT: import("./schema.js").ContractObject<{
43
+ readonly projection: {
44
+ type: "string";
45
+ enum: readonly ("full" | "summary" | "value")[];
46
+ description?: string;
47
+ };
48
+ readonly path: import("./schema.js").StringProperty;
49
+ readonly value_cursor: import("./schema.js").StringProperty;
50
+ readonly max_chars: {
51
+ type: "integer";
52
+ description?: string;
53
+ minimum?: number;
54
+ maximum?: number;
55
+ "x-clamp"?: true;
56
+ };
57
+ readonly status: import("./schema.js").StringProperty;
58
+ readonly conversation: import("./schema.js").StringProperty;
59
+ readonly conversation_id: import("./schema.js").StringProperty;
60
+ readonly limit: {
61
+ type: "integer";
62
+ description?: string;
63
+ minimum?: number;
64
+ maximum?: number;
65
+ "x-clamp"?: true;
66
+ };
67
+ readonly offset: {
68
+ type: "integer";
69
+ description?: string;
70
+ minimum?: number;
71
+ maximum?: number;
72
+ "x-clamp"?: true;
73
+ };
74
+ readonly draft_id: import("./schema.js").StringProperty;
75
+ }, never>;
76
+ export declare const WHATSAPP_DRAFTS_MCP_CONTRACT: import("./schema.js").ContractObject<{
77
+ readonly projection: {
78
+ type: "string";
79
+ enum: readonly ("full" | "summary" | "value")[];
80
+ description?: string;
81
+ };
82
+ readonly path: import("./schema.js").StringProperty;
83
+ readonly value_cursor: import("./schema.js").StringProperty;
84
+ readonly max_chars: {
85
+ type: "integer";
86
+ description?: string;
87
+ minimum?: number;
88
+ maximum?: number;
89
+ "x-clamp"?: true;
90
+ };
91
+ readonly status: import("./schema.js").StringProperty;
92
+ readonly conversation: import("./schema.js").StringProperty;
93
+ readonly conversation_id: import("./schema.js").StringProperty;
94
+ readonly offset: {
95
+ type: "integer";
96
+ description?: string;
97
+ minimum?: number;
98
+ maximum?: number;
99
+ "x-clamp"?: true;
100
+ };
101
+ readonly draft_id: import("./schema.js").StringProperty;
102
+ readonly limit: {
103
+ type: "integer";
104
+ description?: string;
105
+ minimum?: number;
106
+ maximum?: number;
107
+ "x-clamp"?: true;
108
+ };
109
+ }, never>;
110
+ /** Preserve native canonical/alias precedence, including a canonical blank. */
111
+ export declare function whatsappReadQueryParams(input: Record<string, unknown>): URLSearchParams;
@@ -0,0 +1,33 @@
1
+ import { bool, enumOf, int, obj, str } from "./schema.js";
2
+ import { STORED_READ_WINDOW_FIELDS } from "./stored-read-window.js";
3
+ export const WHATSAPP_ACCOUNT_STATUSES = ["active", "paused", "disconnected", "restricted", "credentials_required"];
4
+ export const WHATSAPP_ACCOUNTS_READ_CONTRACT = obj({
5
+ status: enumOf(WHATSAPP_ACCOUNT_STATUSES, { description: "Exact sender status; takes precedence over include_disconnected." }),
6
+ include_usage: bool({ description: "Stored usage counts; default true." }),
7
+ include_disconnected: bool({ description: "Include retained disconnected accounts; default false. Presence is not send readiness." }),
8
+ tag: str({ description: "Comma-separated workspace tags, ANY-match." }),
9
+ account_id: str({ description: "Exact connection id from this filtered account list." }),
10
+ limit: int({ minimum: 1, maximum: 200 }),
11
+ offset: int({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: "Matching account offset; live changes can shift pages." }),
12
+ ...STORED_READ_WINDOW_FIELDS,
13
+ });
14
+ export const WHATSAPP_DRAFT_STATUSES = ["queued", "edited", "approved", "sent", "rejected"];
15
+ export const WHATSAPP_DRAFTS_READ_CONTRACT = obj({
16
+ status: str({ description: `CSV current statuses: ${WHATSAPP_DRAFT_STATUSES.join(",")}; default queued,edited. No transition audit.` }),
17
+ conversation: str({ description: "Legacy local WhatsApp conversation UUID alias." }),
18
+ conversation_id: str({ description: "Local WhatsApp conversation UUID; takes precedence over conversation. Retired native refs remain valid." }),
19
+ // Existing CLI accepts any positive integer; native owner clamps to 200.
20
+ // Retain that admission while the already-stricter MCP schema stays bounded.
21
+ limit: int({ minimum: 1, description: "Default 50; native maximum 200." }),
22
+ offset: int({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER, description: "Matching agent draft offset; live changes can shift pages." }),
23
+ draft_id: str({ description: "Exact eligible agent draft UUID under the same status/conversation filters." }),
24
+ ...STORED_READ_WINDOW_FIELDS,
25
+ });
26
+ export const WHATSAPP_DRAFTS_MCP_CONTRACT = obj({
27
+ ...WHATSAPP_DRAFTS_READ_CONTRACT.properties,
28
+ limit: int({ minimum: 1, maximum: 200 }),
29
+ });
30
+ /** Preserve native canonical/alias precedence, including a canonical blank. */
31
+ export function whatsappReadQueryParams(input) {
32
+ return new URLSearchParams(Object.entries(input).filter(([, value]) => value !== undefined && value !== null).map(([key, value]) => [key, String(value)]));
33
+ }
@@ -1,9 +1,10 @@
1
1
  /**
2
- * `POST /api/cli/settings/pages`: the web app's sidebar pages a workspace hides
3
- * for every member (Settings → Customise; Philipp, 2026-09-30). Presentation
4
- * only: a hidden page leaves the sidebar and the ⌘K palette, and stays
5
- * reachable by URL and deep-link. Admins and owners change it; every member
6
- * reads it through `GET` on the same route.
2
+ * `POST /api/cli/settings/pages`: the web app's sidebar pages one person hides
3
+ * for themselves in one workspace (Settings → Customise). Per user and per
4
+ * workspace since Philipp's 2026-10-02 decision; it was workspace-wide before.
5
+ * Presentation only: a hidden page leaves that person's sidebar and ⌘K palette,
6
+ * and stays reachable by URL and deep-link. Every member reads and changes their
7
+ * own list through `GET` and `POST` on the same route, never anyone else's.
7
8
  */
8
9
  import type { OperationSpec } from "./operations.js";
9
10
  import { type Infer } from "./schema.js";
@@ -4,7 +4,7 @@ export const WORKSPACE_PAGES_UPDATE_CONTRACT = obj({
4
4
  hide: arr(pageRef, {
5
5
  minItems: 1,
6
6
  maxItems: 100,
7
- description: "Pages to hide for everyone, each by its path (/unibox) or name (Unibox) from the page list. Hiding a section hides its sub-pages.",
7
+ description: "Pages to hide from your own sidebar, each by its path (/unibox) or name (Unibox) from the page list. Hiding a section hides its sub-pages.",
8
8
  }),
9
9
  show: arr(pageRef, {
10
10
  minItems: 1,
@@ -23,8 +23,8 @@ export const WORKSPACE_PAGES_GET_OPERATION = {
23
23
  method: "GET",
24
24
  contract: obj({}),
25
25
  generated: { cli: false, mcp: false },
26
- cli: { path: ["settings", "pages", "list"], summary: "Page visibility in the web app's sidebar navigation menu (Settings → Customise): hidden_pages, whether you may customize it (can_change: admins and owners), then every page (path, name, section, parent) with hidden (hidden itself) and visible (shown in the sidebar; false under a hidden section). Read-only, 0 credits." },
27
- mcp: { name: "oxygen_settings_pages_get", title: "Oxygen: List Sidebar Pages", description: "Lists the OXYGEN web app's sidebar navigation pages (Settings → Customise) with whether this workspace hides each one for everyone, and can_change (admins and owners). Free. Change with oxygen_settings_pages_update.", pack: "workspace" },
26
+ cli: { path: ["settings", "pages", "list"], summary: "Page visibility in your own sidebar navigation menu in this workspace (Settings → Customise; only you see your choices): hidden_pages, then every page (path, name, section, parent) with hidden (you hid it) and visible (shown in your sidebar; false under a hidden section). Read-only, 0 credits." },
27
+ mcp: { name: "oxygen_settings_pages_get", title: "Oxygen: List Sidebar Pages", description: "Lists the OXYGEN web app's sidebar navigation pages (Settings → Customise) with which ones you hid from your own sidebar in this workspace; only you see your choices. Free. Change with oxygen_settings_pages_update.", pack: "workspace" },
28
28
  mutates: false,
29
29
  spends: false,
30
30
  };
@@ -36,12 +36,12 @@ export const WORKSPACE_PAGES_UPDATE_OPERATION = {
36
36
  generated: { cli: false, mcp: false },
37
37
  cli: {
38
38
  path: ["settings", "pages", "set"],
39
- summary: "Hide or show pages in the web app's sidebar navigation menu for everyone in this workspace (Settings → Customise): --hide Unibox,Publishing, --show Unibox, --show-all. Name pages by path or name from `settings pages list`; hiding a section hides its sub-pages. Nothing is deleted and hidden pages stay reachable by URL. Admins and owners only; free.",
39
+ summary: "Hide or show pages in your own sidebar navigation menu in this workspace (Settings → Customise): --hide Unibox,Publishing, --show Unibox, --show-all. Only your sidebar changes; teammates keep theirs. Name pages by path or name from `settings pages list`; hiding a section hides its sub-pages. Nothing is deleted and hidden pages stay reachable by URL. Free.",
40
40
  },
41
41
  mcp: {
42
42
  name: "oxygen_settings_pages_update",
43
43
  title: "Oxygen: Hide or Show Sidebar Pages",
44
- description: "Hides or shows pages in the OXYGEN web app's sidebar navigation for everyone in this workspace (Settings → Customise). hide/show take page paths or names from oxygen_settings_pages_get; show_all shows every page again. Nothing is deleted and hidden pages stay reachable by URL. Admins and owners only. Free.",
44
+ description: "Hides or shows pages in your own OXYGEN sidebar navigation in this workspace (Settings → Customise); teammates' sidebars do not change. hide/show take page paths or names from oxygen_settings_pages_get; show_all shows every page again. Nothing is deleted and hidden pages stay reachable by URL. Free.",
45
45
  pack: "workspace",
46
46
  },
47
47
  mutates: true,
@@ -1,4 +1,5 @@
1
- export type CopilotPlaybookSlug = "tam-sourcing" | "linkedin-content-strategy" | "inbound-led-outbound" | "signal-based-outbound";
1
+ import { COPILOT_PLAYBOOKS_GENERATED } from "./copilot-playbooks.generated.js";
2
+ export type CopilotPlaybookSlug = (typeof COPILOT_PLAYBOOKS_GENERATED)[number]["slug"];
2
3
  export type CopilotPlaybook = {
3
4
  /**
4
5
  * The skill slug the Copilot loads through its `skills` tool, and the
@@ -7,6 +8,8 @@ export type CopilotPlaybook = {
7
8
  slug: CopilotPlaybookSlug;
8
9
  /** Customer-facing name, exactly as it is spoken in the composer prompt and shown on the Home chip. */
9
10
  title: string;
11
+ /** The featured catalog recipe that carries this play: its kit, prerequisites and approval gates. */
12
+ recipeSlug: string;
10
13
  };
11
14
  export declare const COPILOT_PLAYBOOKS: readonly CopilotPlaybook[];
12
15
  /**
@@ -0,0 +1,17 @@
1
+ export declare const COPILOT_PLAYBOOKS_GENERATED: readonly [{
2
+ readonly slug: "tam-sourcing";
3
+ readonly title: "TAM sourcing";
4
+ readonly recipeSlug: "icp-to-account-map";
5
+ }, {
6
+ readonly slug: "linkedin-content-strategy";
7
+ readonly title: "LinkedIn content strategy";
8
+ readonly recipeSlug: "founder-posting-system";
9
+ }, {
10
+ readonly slug: "inbound-led-outbound";
11
+ readonly title: "Inbound-led outbound";
12
+ readonly recipeSlug: "inbound-led-outbound";
13
+ }, {
14
+ readonly slug: "signal-based-outbound";
15
+ readonly title: "Signal-based outbound";
16
+ readonly recipeSlug: "daily-warm-lead-queue";
17
+ }];
@@ -0,0 +1,26 @@
1
+ // GENERATED by apps/web/scripts/generate-copilot-playbooks.ts from the recipe
2
+ // catalog (featured recipes: `featured_order`, `label`, `playbook` in
3
+ // apps/web/src/agent-skills/oxygen-recipes/recipes).
4
+ // Do not edit by hand — run `npm run skills:metadata -w @oxygen/web`.
5
+ export const COPILOT_PLAYBOOKS_GENERATED = [
6
+ {
7
+ slug: "tam-sourcing",
8
+ title: "TAM sourcing",
9
+ recipeSlug: "icp-to-account-map",
10
+ },
11
+ {
12
+ slug: "linkedin-content-strategy",
13
+ title: "LinkedIn content strategy",
14
+ recipeSlug: "founder-posting-system",
15
+ },
16
+ {
17
+ slug: "inbound-led-outbound",
18
+ title: "Inbound-led outbound",
19
+ recipeSlug: "inbound-led-outbound",
20
+ },
21
+ {
22
+ slug: "signal-based-outbound",
23
+ title: "Signal-based outbound",
24
+ recipeSlug: "daily-warm-lead-queue",
25
+ },
26
+ ];
@@ -1,35 +1,5 @@
1
- // The four end-to-end GTM playbooks the Workspace Copilot can run, as pure,
2
- // client-safe data. One canonical copy is read by three consumers: the Home
3
- // cards (which pre-write the prompt below into the composer), the Copilot's
4
- // system prompt (which maps the spoken title back to the skill slug the model
5
- // loads through its `skills` tool), and the apps/web gate that checks every slug
6
- // here has a served playbook file with a matching frontmatter `name`.
7
- //
8
- // A playbook is the operator contract for one whole motion — stages, owners,
9
- // exact commands, caps, approval gates, stop conditions, learning loop — served
10
- // as a product skill (`apps/web/src/agent-skills/oxygen-playbooks/playbooks/`).
11
- // Loading one is never permission to spend or send: every gate inside it still
12
- // fires. Founder decision 2026-09-16: Home offers exactly these four, above the
13
- // composer as compact glass chips (title only), and a chip writes the sentence
14
- // into the box rather than starting a session, so there stays exactly one door.
15
- export const COPILOT_PLAYBOOKS = [
16
- {
17
- slug: "tam-sourcing",
18
- title: "TAM sourcing",
19
- },
20
- {
21
- slug: "linkedin-content-strategy",
22
- title: "LinkedIn content strategy",
23
- },
24
- {
25
- slug: "inbound-led-outbound",
26
- title: "Inbound-led outbound",
27
- },
28
- {
29
- slug: "signal-based-outbound",
30
- title: "Signal-based outbound",
31
- },
32
- ];
1
+ import { COPILOT_PLAYBOOKS_GENERATED } from "./copilot-playbooks.generated.js";
2
+ export const COPILOT_PLAYBOOKS = COPILOT_PLAYBOOKS_GENERATED;
33
3
  /**
34
4
  * The sentence a Home card writes into the composer. It names the skill the way
35
5
  * a person would say it, and the Copilot's prompt maps that title back to the
@@ -76,6 +76,11 @@ export type FeatureGateResolution = {
76
76
  * (`packages/shared/src/repricing.ts`). A remotely flippable value would let
77
77
  * a mis-click move every customer's prices; it changes only through Doppler,
78
78
  * with explicit human approval.
79
+ * - OXYGEN_FIRST_MONTH_OFFER_COUPON — (b). Names the Stripe coupon that takes
80
+ * $50 off a new customer's first $99 month (`apps/web/src/lib/billing/
81
+ * first-month-offer.ts`, founder decision 2026-10-02). A remotely flippable
82
+ * value would let a mis-click discount every eligible checkout or withdraw an
83
+ * offer a visitor is counting down to; it changes only through Doppler.
79
84
  * - OXYGEN_CREDIT_GATE_ENABLED — (b). Lets a connect through on the balance
80
85
  * instead of a sending seat (`packages/shared/src/credit-gate.ts`). A wrong
81
86
  * "on" while commitments do not bill every connectable kind would admit
@@ -67,6 +67,11 @@
67
67
  * (`packages/shared/src/repricing.ts`). A remotely flippable value would let
68
68
  * a mis-click move every customer's prices; it changes only through Doppler,
69
69
  * with explicit human approval.
70
+ * - OXYGEN_FIRST_MONTH_OFFER_COUPON — (b). Names the Stripe coupon that takes
71
+ * $50 off a new customer's first $99 month (`apps/web/src/lib/billing/
72
+ * first-month-offer.ts`, founder decision 2026-10-02). A remotely flippable
73
+ * value would let a mis-click discount every eligible checkout or withdraw an
74
+ * offer a visitor is counting down to; it changes only through Doppler.
70
75
  * - OXYGEN_CREDIT_GATE_ENABLED — (b). Lets a connect through on the balance
71
76
  * instead of a sending seat (`packages/shared/src/credit-gate.ts`). A wrong
72
77
  * "on" while commitments do not bill every connectable kind would admit
@@ -91,6 +96,7 @@ export const NEVER_FLAGGABLE = [
91
96
  "OXYGEN_TRIGGER_DEFAULT_CAPS",
92
97
  "OXYGEN_BYOK_DAILY_CAPS",
93
98
  "OXYGEN_REPRICING_2026_09_EFFECTIVE_AT",
99
+ "OXYGEN_FIRST_MONTH_OFFER_COUPON",
94
100
  "OXYGEN_CREDIT_GATE_ENABLED",
95
101
  "OXYGEN_MCP_EVENTS_ENABLED",
96
102
  ];
@@ -0,0 +1,34 @@
1
+ /**
2
+ * The first-month offer's 24-hour window and the browser's record of it, with no
3
+ * plan catalog. The offer banner and the app shell run in the browser and import
4
+ * this; importing `first-month-offer.ts` there pulled the whole billing catalog
5
+ * into every dashboard route (+16 KB gzip) and every website page (+8 KB),
6
+ * which the release battery's bundle budget refused on 2026-10-03. Prices stay
7
+ * in `first-month-offer.ts`: a server computes the banner's headline with
8
+ * `firstMonthOfferHeadline()` and hands it down.
9
+ */
10
+ /** How long a person has to claim the offer after first seeing it. */
11
+ export declare const FIRST_MONTH_OFFER_WINDOW_MS: number;
12
+ /**
13
+ * First-party cookie holding when this browser first saw the offer, in epoch
14
+ * milliseconds. Readable by the page so the countdown needs no request; signup
15
+ * copies it onto the workspace, after which the workspace's own record decides.
16
+ * Clearing it starts a new window, which the founder accepted (2026-10-02).
17
+ */
18
+ export declare const FIRST_MONTH_OFFER_SEEN_COOKIE = "ox_first_month_offer_seen";
19
+ /** How long the browser keeps the start, so an expired window stays expired. */
20
+ export declare const FIRST_MONTH_OFFER_SEEN_MAX_AGE_SECONDS: number;
21
+ /** What the banner says: "50% off your first month on every plan from $99". */
22
+ export type FirstMonthOfferHeadline = {
23
+ percentOff: number;
24
+ /** The cheapest size the offer covers, at its regular monthly price. */
25
+ fromCents: number;
26
+ };
27
+ /** When a window that started at `startedAtMs` closes. */
28
+ export declare function firstMonthOfferEndsAt(startedAtMs: number): number;
29
+ /**
30
+ * A remembered start, or null when there is none worth trusting: not a number,
31
+ * in the future (beyond a minute of clock skew), or older than the cookie may
32
+ * live. A start the browser invented is no worse than a cleared cookie.
33
+ */
34
+ export declare function parseFirstMonthOfferStart(raw: string | null | undefined, nowMs: number): number | null;