@oxygen-agent/cli 1.1014.1343 → 1.1014.1451

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 (82) hide show
  1. package/README.md +1 -1
  2. package/dist/command-families/program.js +2 -0
  3. package/dist/command-families/tables/register-reads.js +1 -1
  4. package/dist/command-manifest.js +1 -0
  5. package/dist/command-support.js +4 -2
  6. package/dist/commands/blueprints.js +10 -6
  7. package/dist/commands/clay-migration.d.ts +3 -0
  8. package/dist/commands/clay-migration.js +22 -0
  9. package/dist/commands/crm.js +5 -2
  10. package/dist/commands/dashboards.js +4 -2
  11. package/dist/commands/db.js +1 -1
  12. package/dist/commands/directory.js +7 -4
  13. package/dist/commands/domains.js +1 -1
  14. package/dist/commands/inbox.js +5 -2
  15. package/dist/commands/prompts.js +4 -2
  16. package/dist/commands/publishing.js +5 -2
  17. package/dist/commands/sequences.js +13 -4
  18. package/dist/commands/support.js +30 -19
  19. package/dist/customer-operation-commands.js +0 -42
  20. package/dist/http-client.js +4 -1
  21. package/dist/workspace-placement-commands.js +1 -1
  22. package/node_modules/@oxygen/shared/dist/capability-args.d.ts +19 -0
  23. package/node_modules/@oxygen/shared/dist/capability-args.js +73 -0
  24. package/node_modules/@oxygen/shared/dist/capability-discovery.js +15 -2
  25. package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.d.ts +3 -0
  26. package/node_modules/@oxygen/shared/dist/clay-migration-auth-state.js +15 -0
  27. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +2 -1
  28. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.d.ts +71 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/clay-migration.js +60 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -6
  31. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +5 -1
  32. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +25 -2
  33. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +1 -1
  34. package/node_modules/@oxygen/shared/dist/dev-eval-context.js +1 -0
  35. package/node_modules/@oxygen/shared/dist/enrollment-origin.d.ts +3 -1
  36. package/node_modules/@oxygen/shared/dist/enrollment-origin.js +2 -0
  37. package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +2 -0
  38. package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.d.ts +9 -2
  39. package/node_modules/@oxygen/shared/dist/guided-trial-billing.d.ts +10 -2
  40. package/node_modules/@oxygen/shared/dist/guided-trial-billing.js +2 -5
  41. package/node_modules/@oxygen/shared/dist/hosted-ai.js +8 -4
  42. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -0
  43. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +1 -0
  44. package/node_modules/@oxygen/shared/dist/onboarding-plays.d.ts +68 -0
  45. package/node_modules/@oxygen/shared/dist/onboarding-plays.js +114 -0
  46. package/node_modules/@oxygen/shared/dist/onboarding-research.d.ts +42 -0
  47. package/node_modules/@oxygen/shared/dist/onboarding-research.js +62 -0
  48. package/node_modules/@oxygen/shared/dist/placement-copy.d.ts +8 -0
  49. package/node_modules/@oxygen/shared/dist/placement-copy.js +16 -0
  50. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +8 -0
  51. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +8 -0
  52. package/node_modules/@oxygen/shared/dist/sequences.d.ts +30 -2
  53. package/node_modules/@oxygen/shared/dist/sequences.js +67 -7
  54. package/node_modules/@oxygen/shared/dist/teammates.d.ts +3 -1
  55. package/node_modules/@oxygen/shared/dist/teammates.js +4 -2
  56. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +19 -40
  57. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  58. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  59. package/node_modules/@oxygen/shared/dist/workspace-placement.js +4 -0
  60. package/package.json +1 -1
  61. package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.d.ts +0 -30
  62. package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.js +0 -53
  63. package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.d.ts +0 -120
  64. package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.js +0 -48
  65. package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.d.ts +0 -267
  66. package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.js +0 -63
  67. package/node_modules/@oxygen/shared/dist/guided-company-files.d.ts +0 -13
  68. package/node_modules/@oxygen/shared/dist/guided-company-files.js +0 -14
  69. package/node_modules/@oxygen/shared/dist/guided-first-sourcing.d.ts +0 -116
  70. package/node_modules/@oxygen/shared/dist/guided-first-sourcing.js +0 -11
  71. package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.d.ts +0 -4
  72. package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.js +0 -42
  73. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.d.ts +0 -147
  74. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.d.ts +0 -12
  75. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.js +0 -83
  76. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.js +0 -44
  77. package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.d.ts +0 -7
  78. package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.js +0 -13
  79. package/node_modules/@oxygen/shared/dist/guided-onboarding-recommendations.d.ts +0 -54
  80. package/node_modules/@oxygen/shared/dist/guided-onboarding-recommendations.js +0 -100
  81. package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.d.ts +0 -4
  82. package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.js +0 -34
@@ -0,0 +1,68 @@
1
+ export declare const ONBOARDING_PROPOSE_PLAYS_TOOL_NAME = "propose_plays";
2
+ export declare const ONBOARDING_PLAY_LIMITS: {
3
+ readonly minPlays: 2;
4
+ readonly maxPlays: 4;
5
+ readonly titleChars: 80;
6
+ readonly lineChars: 240;
7
+ readonly slugChars: 120;
8
+ };
9
+ export type OnboardingPlay = {
10
+ id: string;
11
+ title: string;
12
+ /** Who it targets: segment and buyer. */
13
+ audience: string;
14
+ /** The signal or trigger it acts on. */
15
+ trigger: string | null;
16
+ /** The angle: what the message offers them. */
17
+ angle: string;
18
+ /** Why it fits this company, from the research. */
19
+ why: string | null;
20
+ /** What running it does first. */
21
+ firstStep: string;
22
+ /** The play's Knowledge page, when one was written. */
23
+ page: string | null;
24
+ };
25
+ export type OnboardingPlayProposal = {
26
+ id: string;
27
+ plays: OnboardingPlay[];
28
+ };
29
+ /** The proposal a chat is waiting on, as every surface reads it. */
30
+ export type PendingOnboardingPlayProposal = OnboardingPlayProposal & {
31
+ turnId: string;
32
+ };
33
+ /** The tool's input schema, as the model reads it. */
34
+ export declare const ONBOARDING_PROPOSE_PLAYS_INPUT_SCHEMA: Record<string, unknown>;
35
+ /** Validate the plays the model proposed, minting ids. */
36
+ export declare function normalizeOnboardingPlayProposal(args: Record<string, unknown>, mintId: () => string): {
37
+ ok: true;
38
+ proposal: OnboardingPlayProposal;
39
+ } | {
40
+ ok: false;
41
+ message: string;
42
+ };
43
+ /** The proposal as the turn's reply: what the CLI and MCP print, and what the next turn reads back. */
44
+ export declare function renderOnboardingPlayProposalText(proposal: OnboardingPlayProposal): string;
45
+ /** Read a stored proposal back, refusing anything malformed. */
46
+ export declare function readOnboardingPlayProposal(value: unknown): OnboardingPlayProposal | null;
47
+ /** The message a chosen play sends, as the person's own. */
48
+ export declare function onboardingPlayMessage(play: Pick<OnboardingPlay, "title" | "firstStep" | "page">): string;
49
+ type ProjectionTurn = {
50
+ id: string;
51
+ status: string;
52
+ turnIndex: number;
53
+ };
54
+ type ProjectionEvent = {
55
+ turnId: string | null;
56
+ kind: string;
57
+ payload: Record<string, unknown>;
58
+ };
59
+ /**
60
+ * The plays the chat is waiting on, or null. Like a question, only the newest
61
+ * turn can be waiting, and only when it completed by proposing: choosing one
62
+ * sends a new message, which is the end of the wait.
63
+ */
64
+ export declare function projectPendingOnboardingPlayProposal(input: {
65
+ turns: readonly ProjectionTurn[];
66
+ events: readonly ProjectionEvent[];
67
+ }): PendingOnboardingPlayProposal | null;
68
+ export {};
@@ -0,0 +1,114 @@
1
+ // Agent-led onboarding (Philipp, 2026-10-19): the onboarding turn ends with a
2
+ // few proposed outbound plays as cards. Choosing one has the Oxygent run it in
3
+ // the chat, as an ordinary message the person sent; typing steers instead.
4
+ //
5
+ // One owner for the contract, because four readers depend on it: the tool that
6
+ // validates the model's proposal, the session read that projects the pending
7
+ // one, the card that shows it and the route that runs the chosen play.
8
+ export const ONBOARDING_PROPOSE_PLAYS_TOOL_NAME = "propose_plays";
9
+ export const ONBOARDING_PLAY_LIMITS = {
10
+ minPlays: 2,
11
+ maxPlays: 4,
12
+ titleChars: 80,
13
+ lineChars: 240,
14
+ slugChars: 120,
15
+ };
16
+ const line = (description) => ({ type: "string", minLength: 1, maxLength: ONBOARDING_PLAY_LIMITS.lineChars, description });
17
+ /** The tool's input schema, as the model reads it. */
18
+ export const ONBOARDING_PROPOSE_PLAYS_INPUT_SCHEMA = {
19
+ type: "object",
20
+ additionalProperties: false,
21
+ properties: {
22
+ plays: {
23
+ type: "array",
24
+ minItems: ONBOARDING_PLAY_LIMITS.minPlays,
25
+ maxItems: ONBOARDING_PLAY_LIMITS.maxPlays,
26
+ items: {
27
+ type: "object",
28
+ additionalProperties: false,
29
+ properties: {
30
+ title: { type: "string", minLength: 1, maxLength: ONBOARDING_PLAY_LIMITS.titleChars, description: "Short name, e.g. \"New VP Sales at Series A SaaS\"." },
31
+ audience: line("Who: the segment and the buyer persona."),
32
+ trigger: line("The signal or event that makes now the moment, if any."),
33
+ angle: line("What the outreach offers them, in one line."),
34
+ why: line("Why this fits this company, from the research."),
35
+ first_step: line("What running it does first, e.g. \"Find 50 matching accounts and their VP Sales\"."),
36
+ page: { type: "string", maxLength: ONBOARDING_PLAY_LIMITS.slugChars, description: "The play's Knowledge page slug, if you wrote one." },
37
+ },
38
+ required: ["title", "audience", "angle", "first_step"],
39
+ },
40
+ },
41
+ },
42
+ required: ["plays"],
43
+ };
44
+ const clean = (value, max) => typeof value === "string" ? value.replace(/\s+/g, " ").trim().slice(0, max) : "";
45
+ /** Validate the plays the model proposed, minting ids. */
46
+ export function normalizeOnboardingPlayProposal(args, mintId) {
47
+ const raw = Array.isArray(args.plays) ? args.plays : [];
48
+ const plays = [];
49
+ for (const entry of raw) {
50
+ if (!entry || typeof entry !== "object" || Array.isArray(entry))
51
+ continue;
52
+ const record = entry;
53
+ const title = clean(record.title, ONBOARDING_PLAY_LIMITS.titleChars);
54
+ const audience = clean(record.audience, ONBOARDING_PLAY_LIMITS.lineChars);
55
+ const angle = clean(record.angle, ONBOARDING_PLAY_LIMITS.lineChars);
56
+ const firstStep = clean(record.first_step ?? record.firstStep, ONBOARDING_PLAY_LIMITS.lineChars);
57
+ if (!title || !audience || !angle || !firstStep)
58
+ continue;
59
+ const trigger = clean(record.trigger, ONBOARDING_PLAY_LIMITS.lineChars);
60
+ const why = clean(record.why, ONBOARDING_PLAY_LIMITS.lineChars);
61
+ const page = clean(record.page, ONBOARDING_PLAY_LIMITS.slugChars);
62
+ plays.push({ id: typeof record.id === "string" && record.id ? record.id : mintId(), title, audience, trigger: trigger || null,
63
+ angle, why: why || null, firstStep, page: /^[a-z0-9][a-z0-9-]*$/.test(page) ? page : null });
64
+ }
65
+ if (plays.length < ONBOARDING_PLAY_LIMITS.minPlays || plays.length > ONBOARDING_PLAY_LIMITS.maxPlays) {
66
+ return { ok: false, message: `Propose ${ONBOARDING_PLAY_LIMITS.minPlays} to ${ONBOARDING_PLAY_LIMITS.maxPlays} plays, each with a title, audience, angle and first step.` };
67
+ }
68
+ return { ok: true, proposal: { id: typeof args.id === "string" && args.id ? args.id : mintId(), plays } };
69
+ }
70
+ /** The proposal as the turn's reply: what the CLI and MCP print, and what the next turn reads back. */
71
+ export function renderOnboardingPlayProposalText(proposal) {
72
+ return [
73
+ "Plays I'd run first:",
74
+ ...proposal.plays.map((play, index) => `${index + 1}. **${play.title}**: ${play.audience}. ${play.angle}`),
75
+ "Pick one to start it, or tell me what to change.",
76
+ ].join("\n");
77
+ }
78
+ /** Read a stored proposal back, refusing anything malformed. */
79
+ export function readOnboardingPlayProposal(value) {
80
+ if (!value || typeof value !== "object" || Array.isArray(value))
81
+ return null;
82
+ const record = value;
83
+ if (typeof record.id !== "string" || !record.id)
84
+ return null;
85
+ const normalized = normalizeOnboardingPlayProposal(record, () => "");
86
+ return normalized.ok && normalized.proposal.plays.every((play) => play.id) ? normalized.proposal : null;
87
+ }
88
+ /** The message a chosen play sends, as the person's own. */
89
+ export function onboardingPlayMessage(play) {
90
+ return `Run the play "${play.title}". Start with: ${play.firstStep}${play.page ? ` (see [[${play.page}]])` : ""}`;
91
+ }
92
+ /**
93
+ * The plays the chat is waiting on, or null. Like a question, only the newest
94
+ * turn can be waiting, and only when it completed by proposing: choosing one
95
+ * sends a new message, which is the end of the wait.
96
+ */
97
+ export function projectPendingOnboardingPlayProposal(input) {
98
+ const newest = input.turns.reduce((latest, turn) => (latest === null || turn.turnIndex > latest.turnIndex ? turn : latest), null);
99
+ if (!newest || newest.status !== "completed")
100
+ return null;
101
+ for (let index = input.events.length - 1; index >= 0; index -= 1) {
102
+ const event = input.events[index];
103
+ if (!event || event.turnId !== newest.id || event.kind !== "tool_call_finished")
104
+ continue;
105
+ if (event.payload.tool !== ONBOARDING_PROPOSE_PLAYS_TOOL_NAME)
106
+ continue;
107
+ const result = event.payload.result;
108
+ if (!result || typeof result !== "object" || result.ok === false)
109
+ continue;
110
+ const proposal = readOnboardingPlayProposal(result.proposal);
111
+ return proposal ? { ...proposal, turnId: newest.id } : null;
112
+ }
113
+ return null;
114
+ }
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Agent-led onboarding's research (Philipp, 2026-10-19; OXYGEN_PRODUCT.md
3
+ * "Agent-led onboarding"): the first "Start onboarding" turn of a guided
4
+ * activation is OXYGEN-funded, and its research sub-agents may search the web,
5
+ * read the company's site and LinkedIn, find its people and write draft
6
+ * Knowledge pages in parallel, without an approval card per call. These bounds
7
+ * are what replaces the card. Shared by the worker (the sub-agents' grant), the
8
+ * tool route (who pays) and the credit rail (the funding policy).
9
+ */
10
+ export declare const ONBOARDING_RESEARCH_PURPOSE = "onboarding_research";
11
+ /** The research sub-agent the onboarding turn delegates to. */
12
+ export declare const ONBOARDING_RESEARCH_SUBAGENT = "onboarding_researcher";
13
+ /** Provider credits OXYGEN pays for one workspace's onboarding research, over its lifetime. */
14
+ export declare const ONBOARDING_RESEARCH_MAX_CREDITS = 300;
15
+ /** Ceiling on any one research call; a call that names a higher one is lowered to it. */
16
+ export declare const ONBOARDING_RESEARCH_MAX_CREDITS_PER_CALL = 15;
17
+ /** The onboarding turn's own and its sub-agents' inference, credited back once the turn settles. */
18
+ export declare const ONBOARDING_RESEARCH_INFERENCE_MAX_CREDITS = 150;
19
+ /**
20
+ * Eight areas at once (offer, ICP, plays, signals, competitors, company
21
+ * LinkedIn, people, the signing-up person), with room for one follow-up pass.
22
+ * Each researcher wraps up after 90 seconds: on the first live run the slowest
23
+ * of eight took 370 seconds and everyone waited on it (2026-10-19).
24
+ */
25
+ export declare const ONBOARDING_RESEARCH_SUBAGENT_LIMITS: {
26
+ readonly maxRuns: 12;
27
+ readonly maxParallelTasks: 8;
28
+ readonly maxInferencesPerRun: 6;
29
+ readonly maxCapabilityCallsPerRun: 20;
30
+ readonly concludeAfterMs: 90000;
31
+ };
32
+ /**
33
+ * Read-only provider tools the research may call through `oxygen_tools_run_live`.
34
+ * Nothing here writes outside OXYGEN; the costly ones (competitor search at 61
35
+ * credits, large exa result sets) stay out or are capped per call.
36
+ */
37
+ export declare const ONBOARDING_RESEARCH_TOOL_IDS: readonly ["serper.search", "exa.search", "exa.contents", "exa.find_similar", "firecrawl.map", "firecrawl.scrape", "web.page_fetch", "scraper.linkedin_company", "scraper.linkedin_profile", "scraper.linkedin_profile_search", "scraper.linkedin_job_search", "blitzapi.employee_search", "blitzapi.person_profile_enrich", "blitzapi.company_profile_enrich", "predictleads.company_news_events", "predictleads.company_job_openings", "predictleads.company_similar_companies"];
38
+ export declare function onboardingResearchToolAllowed(toolId: unknown): boolean;
39
+ /** The hub pages the onboarding Oxygent itself writes; research links to them and never creates them. */
40
+ export declare const ONBOARDING_RESEARCH_HUB_SLUGS: readonly ["company", "offers", "icp", "plays", "signals", "competitors", "team"];
41
+ /** Knowledge page types a research sub-agent may not write: canonical voice and positioning stay proposal-gated. */
42
+ export declare const ONBOARDING_RESEARCH_DENIED_PAGE_TYPES: readonly ["voice", "brand", "positioning", "message_playbook", "playbook"];
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Agent-led onboarding's research (Philipp, 2026-10-19; OXYGEN_PRODUCT.md
3
+ * "Agent-led onboarding"): the first "Start onboarding" turn of a guided
4
+ * activation is OXYGEN-funded, and its research sub-agents may search the web,
5
+ * read the company's site and LinkedIn, find its people and write draft
6
+ * Knowledge pages in parallel, without an approval card per call. These bounds
7
+ * are what replaces the card. Shared by the worker (the sub-agents' grant), the
8
+ * tool route (who pays) and the credit rail (the funding policy).
9
+ */
10
+ export const ONBOARDING_RESEARCH_PURPOSE = "onboarding_research";
11
+ /** The research sub-agent the onboarding turn delegates to. */
12
+ export const ONBOARDING_RESEARCH_SUBAGENT = "onboarding_researcher";
13
+ /** Provider credits OXYGEN pays for one workspace's onboarding research, over its lifetime. */
14
+ export const ONBOARDING_RESEARCH_MAX_CREDITS = 300;
15
+ /** Ceiling on any one research call; a call that names a higher one is lowered to it. */
16
+ export const ONBOARDING_RESEARCH_MAX_CREDITS_PER_CALL = 15;
17
+ /** The onboarding turn's own and its sub-agents' inference, credited back once the turn settles. */
18
+ export const ONBOARDING_RESEARCH_INFERENCE_MAX_CREDITS = 150;
19
+ /**
20
+ * Eight areas at once (offer, ICP, plays, signals, competitors, company
21
+ * LinkedIn, people, the signing-up person), with room for one follow-up pass.
22
+ * Each researcher wraps up after 90 seconds: on the first live run the slowest
23
+ * of eight took 370 seconds and everyone waited on it (2026-10-19).
24
+ */
25
+ export const ONBOARDING_RESEARCH_SUBAGENT_LIMITS = {
26
+ maxRuns: 12,
27
+ maxParallelTasks: 8,
28
+ maxInferencesPerRun: 6,
29
+ maxCapabilityCallsPerRun: 20,
30
+ concludeAfterMs: 90_000,
31
+ };
32
+ /**
33
+ * Read-only provider tools the research may call through `oxygen_tools_run_live`.
34
+ * Nothing here writes outside OXYGEN; the costly ones (competitor search at 61
35
+ * credits, large exa result sets) stay out or are capped per call.
36
+ */
37
+ export const ONBOARDING_RESEARCH_TOOL_IDS = [
38
+ "serper.search",
39
+ "exa.search",
40
+ "exa.contents",
41
+ "exa.find_similar",
42
+ "firecrawl.map",
43
+ "firecrawl.scrape",
44
+ "web.page_fetch",
45
+ "scraper.linkedin_company",
46
+ "scraper.linkedin_profile",
47
+ "scraper.linkedin_profile_search",
48
+ "scraper.linkedin_job_search",
49
+ "blitzapi.employee_search",
50
+ "blitzapi.person_profile_enrich",
51
+ "blitzapi.company_profile_enrich",
52
+ "predictleads.company_news_events",
53
+ "predictleads.company_job_openings",
54
+ "predictleads.company_similar_companies",
55
+ ];
56
+ export function onboardingResearchToolAllowed(toolId) {
57
+ return typeof toolId === "string" && ONBOARDING_RESEARCH_TOOL_IDS.includes(toolId);
58
+ }
59
+ /** The hub pages the onboarding Oxygent itself writes; research links to them and never creates them. */
60
+ export const ONBOARDING_RESEARCH_HUB_SLUGS = ["company", "offers", "icp", "plays", "signals", "competitors", "team"];
61
+ /** Knowledge page types a research sub-agent may not write: canonical voice and positioning stay proposal-gated. */
62
+ export const ONBOARDING_RESEARCH_DENIED_PAGE_TYPES = ["voice", "brand", "positioning", "message_playbook", "playbook"];
@@ -0,0 +1,8 @@
1
+ /** Browser-safe copy policy shared by placement forms and native request owners. */
2
+ export declare const PLACEMENT_COPY_GUIDANCE = "Use the outreach email you would send to a prospect. Import a Sequence step or write realistic copy; do not use delivery-check text or no-reply notices.";
3
+ export declare const PLACEMENT_SAMPLE_COPY: {
4
+ subject: string;
5
+ body_text: string;
6
+ };
7
+ /** Reject obvious diagnostic placeholders, without scoring or rewriting outreach. */
8
+ export declare function placementCopyIssue(subject: string, bodyText: string, bodyHtml?: string): string | null;
@@ -0,0 +1,16 @@
1
+ /** Browser-safe copy policy shared by placement forms and native request owners. */
2
+ export const PLACEMENT_COPY_GUIDANCE = "Use the outreach email you would send to a prospect. Import a Sequence step or write realistic copy; do not use delivery-check text or no-reply notices.";
3
+ export const PLACEMENT_SAMPLE_COPY = {
4
+ subject: "A question about your outbound workflow",
5
+ body_text: "Hi,\n\nHow does your team move from a list of target accounts to personalized outreach today?\n\nWe help teams keep research, enrichment and follow-ups in one workspace so reps spend less time moving data between tools.\n\nWould it be useful to compare that with your current workflow?",
6
+ };
7
+ /** Reject obvious diagnostic placeholders, without scoring or rewriting outreach. */
8
+ export function placementCopyIssue(subject, bodyText, bodyHtml) {
9
+ if (!subject.trim() || !bodyText.trim())
10
+ return PLACEMENT_COPY_GUIDANCE;
11
+ const diagnosticSubject = /^(?:(?:oxygen|emailguard|sendkit|dev|workspace|internal|plain|html)\s+)*(?:(?:inbox[ -]placement|inbox|deliverability|delivery|placement|spam|primary)(?:\s+(?:test|check|probe))|test\s+(?:email|message))(?:\s|[:—–-]|$)/i;
12
+ const diagnosticBody = /\b(?:this\s+(?:(?:email|message)\s+)?is\s+(?:an?\s+|the\s+)?(?:(?:oxygen|dev|workspace)\s+)*(?:inbox[ -]placement|inbox|deliverability|delivery|placement)\s+(?:test|check|probe)|no\s+(?:reply|response)\s+(?:is\s+)?(?:needed|required)|do\s+not\s+reply)\b/i;
13
+ const htmlText = bodyHtml?.replace(/<[^<>]*>/g, " ").replace(/&nbsp;|&#160;/gi, " ") ?? "";
14
+ return diagnosticSubject.test(subject.trim()) || diagnosticBody.test(bodyText) || diagnosticBody.test(htmlText)
15
+ ? PLACEMENT_COPY_GUIDANCE : null;
16
+ }
@@ -25,6 +25,14 @@ export declare const PRODUCT_EVENTS: {
25
25
  readonly ONBOARDING_RESET: "onboarding_reset";
26
26
  readonly ACTIVATION_PATH_SELECTED: "activation_path_selected";
27
27
  readonly AGENT_CONNECTED: "agent_connected";
28
+ /**
29
+ * Browser-side, on onboarding's Install OXYGEN step (`/setup/agent`): what a
30
+ * founder did there before connecting or leaving. `action` is agent_selected,
31
+ * endpoint_copied, prompt_copied, connected, open_agent, open_oxygen,
32
+ * copilot_instead or skipped; `agent` is claude, chatgpt or cli;
33
+ * `seconds_on_step` is a number. A handful per signup, never per poll.
34
+ */
35
+ readonly AGENT_SETUP_ACTION: "agent_setup_action";
28
36
  readonly FIRST_RECORD_CREATED: "first_record_created";
29
37
  /**
30
38
  * The prescribed first play (inbound-led outbound), walked on ANY surface.
@@ -25,6 +25,14 @@ export const PRODUCT_EVENTS = {
25
25
  ONBOARDING_RESET: "onboarding_reset",
26
26
  ACTIVATION_PATH_SELECTED: "activation_path_selected",
27
27
  AGENT_CONNECTED: "agent_connected",
28
+ /**
29
+ * Browser-side, on onboarding's Install OXYGEN step (`/setup/agent`): what a
30
+ * founder did there before connecting or leaving. `action` is agent_selected,
31
+ * endpoint_copied, prompt_copied, connected, open_agent, open_oxygen,
32
+ * copilot_instead or skipped; `agent` is claude, chatgpt or cli;
33
+ * `seconds_on_step` is a number. A handful per signup, never per poll.
34
+ */
35
+ AGENT_SETUP_ACTION: "agent_setup_action",
28
36
  FIRST_RECORD_CREATED: "first_record_created",
29
37
  /**
30
38
  * The prescribed first play (inbound-led outbound), walked on ANY surface.
@@ -701,13 +701,40 @@ export type SequenceStopStep = {
701
701
  * emailsUnsubscribed → { signal: "email_unsubscribed" }
702
702
  * aircallDone → out of scope (no Aircall integration)
703
703
  * Negate any leaf with present:false (e.g. "no email on file" = { has_column: "email", present: false }).
704
+ *
705
+ * A CONTACT leaf `{ has_contact: "email" | "linkedin" | "phone", present? }` asks
706
+ * whether the step that channel needs could reach the lead, resolved exactly as
707
+ * dispatch resolves it: email and phone through the sequence's mapped column with
708
+ * the built-in fallbacks, LinkedIn through the profile captured at enrollment. It
709
+ * is what "If has email / LinkedIn / phone" in the builder writes, so one campaign
710
+ * can route every lead instead of splitting the audience by data.
711
+ */
712
+ export declare const SEQUENCE_CONTACT_CHANNELS: readonly ["email", "linkedin", "phone"];
713
+ export type SequenceContactChannel = (typeof SEQUENCE_CONTACT_CHANNELS)[number];
714
+ /** What a lead can be reached by, as dispatch resolves it; a contact leaf reads this. */
715
+ export type SequenceLeadContacts = Record<SequenceContactChannel, boolean>;
716
+ /**
717
+ * Resolve a lead's reachable contacts the way dispatch does: the mapped email or
718
+ * phone column first, then the built-in keys, and LinkedIn from the profile the
719
+ * enrollment captured (a row value alone never makes a LinkedIn step sendable).
704
720
  */
721
+ export declare function sequenceLeadContacts(input: {
722
+ rowValues: Record<string, unknown> | null | undefined;
723
+ settings?: {
724
+ email_column_key?: string | null;
725
+ phone_column_key?: string | null;
726
+ } | null;
727
+ linkedin: boolean;
728
+ }): SequenceLeadContacts;
705
729
  export type SequenceSignalCondition = {
706
730
  signal: SequenceSignal;
707
731
  present?: boolean;
708
732
  } | {
709
733
  has_column: string;
710
734
  present?: boolean;
735
+ } | {
736
+ has_contact: SequenceContactChannel;
737
+ present?: boolean;
711
738
  } | {
712
739
  all: SequenceSignalCondition[];
713
740
  } | {
@@ -959,9 +986,10 @@ export declare function sequenceTemplateVariables(definition: SequenceDefinition
959
986
  * `rowValues` is optional and backward-compatible: when omitted (undefined), a data
960
987
  * leaf ({ has_column }) evaluates as ABSENT — present:false matches, present:true
961
988
  * does not — so an existing signal-only caller is unaffected. Signal leaves ignore
962
- * rowValues entirely.
989
+ * rowValues entirely. A contact leaf ({ has_contact }) reads `contacts`; without
990
+ * it, the contact counts as absent.
963
991
  */
964
- export declare function evaluateSequenceCondition(condition: SequenceSignalCondition, firedSignals: Iterable<string>, rowValues?: Record<string, unknown> | null): boolean;
992
+ export declare function evaluateSequenceCondition(condition: SequenceSignalCondition, firedSignals: Iterable<string>, rowValues?: Record<string, unknown> | null, contacts?: SequenceLeadContacts): boolean;
965
993
  /** Variant label for an index: 0 → "a" (the base step), 1 → "b", … */
966
994
  export declare function sequenceVariantLabel(index: number): string;
967
995
  type StepVariantContent = Record<string, string>;
@@ -424,6 +424,54 @@ export const SEQUENCE_CRM_IDENTITY_KINDS = [
424
424
  * route connected → then_id, not → else_id (no waiting).
425
425
  */
426
426
  export const SEQUENCE_CONNECTION_BRANCH_CONDITIONS = ["connection_accepted", "already_connected", "open_profile"];
427
+ /**
428
+ * A boolean expression a signal-branch routes on. Two leaf kinds, plus the
429
+ * all/any/not combinators:
430
+ * - a SIGNAL leaf `{ signal, present? }` tests whether an accumulated enrollment
431
+ * signal is present (default) or absent (present:false).
432
+ * - a DATA leaf `{ has_column, present? }` tests whether the lead's source row
433
+ * carries a non-empty value in the named row_values column — present (default)
434
+ * or absent (present:false). This is the LinkedIn/Lemlist "does the lead have an
435
+ * email / phone / … ?" branch: route by what data you HAVE about the lead,
436
+ * evaluated synchronously against the enrollment's row_values (no signal, no
437
+ * wait). A value counts as present when it is a non-empty trimmed string, or any
438
+ * number / boolean; null / undefined / "" / whitespace count as absent.
439
+ *
440
+ * Lemlist condition-key → Oxygen mapping (13 keys), so a Lemlist journey ports
441
+ * across without a lossy translation table:
442
+ * hasEmailAddress → { has_column: "email" } (or any SEQUENCE_EMAIL_COLUMN_KEYS key)
443
+ * hasLinkedinUrl → { has_column: "<linkedin url column>" }
444
+ * hasPhoneNumber → { has_column: "phone" } (or any SEQUENCE_PHONE_COLUMN_KEYS key)
445
+ * linkedinInviteAccepted → a CONNECTION branch (condition: "connection_accepted")
446
+ * opened → { signal: "email_opened" }
447
+ * clicked → { signal: "email_clicked" }
448
+ * replied → { signal: "email_replied" } (or linkedin_replied / whatsapp_replied)
449
+ * meetingBooked → { signal: "meeting_booked" }
450
+ * emailsUnsubscribed → { signal: "email_unsubscribed" }
451
+ * aircallDone → out of scope (no Aircall integration)
452
+ * Negate any leaf with present:false (e.g. "no email on file" = { has_column: "email", present: false }).
453
+ *
454
+ * A CONTACT leaf `{ has_contact: "email" | "linkedin" | "phone", present? }` asks
455
+ * whether the step that channel needs could reach the lead, resolved exactly as
456
+ * dispatch resolves it: email and phone through the sequence's mapped column with
457
+ * the built-in fallbacks, LinkedIn through the profile captured at enrollment. It
458
+ * is what "If has email / LinkedIn / phone" in the builder writes, so one campaign
459
+ * can route every lead instead of splitting the audience by data.
460
+ */
461
+ export const SEQUENCE_CONTACT_CHANNELS = ["email", "linkedin", "phone"];
462
+ /**
463
+ * Resolve a lead's reachable contacts the way dispatch does: the mapped email or
464
+ * phone column first, then the built-in keys, and LinkedIn from the profile the
465
+ * enrollment captured (a row value alone never makes a LinkedIn step sendable).
466
+ */
467
+ export function sequenceLeadContacts(input) {
468
+ const phoneKey = input.settings?.phone_column_key ?? null;
469
+ return {
470
+ email: recipientEmailFromRow(input.rowValues, input.settings?.email_column_key ?? null) !== null,
471
+ linkedin: input.linkedin,
472
+ phone: callPhoneFromRow(input.rowValues, phoneKey) !== null || whatsAppAttendeeIdFromRow(input.rowValues, phoneKey) !== null,
473
+ };
474
+ }
427
475
  const MAX_SEQUENCE_STEPS = 50;
428
476
  const MAX_TEMPLATE_LENGTH = 8_000;
429
477
  // The Premium ceiling, and only an authoring hint: the limit that actually
@@ -835,7 +883,7 @@ function collectConditionSignals(condition, out) {
835
883
  // it contributes nothing — and must be handled BEFORE the not-fallthrough below
836
884
  // (which would otherwise read condition.not off a data leaf). This is also why
837
885
  // the native-untracked engagement-gate walker never trips on a has_column branch.
838
- else if ("has_column" in condition)
886
+ else if ("has_column" in condition || "has_contact" in condition)
839
887
  return;
840
888
  else if ("all" in condition)
841
889
  condition.all.forEach((c) => collectConditionSignals(c, out));
@@ -1810,6 +1858,13 @@ function normalizeCondition(raw, path, issues, depth = 0) {
1810
1858
  return undefined;
1811
1859
  return { has_column: key, present: raw.present === false ? false : true };
1812
1860
  }
1861
+ if (raw.has_contact !== undefined) {
1862
+ if (typeof raw.has_contact !== "string" || !SEQUENCE_CONTACT_CHANNELS.includes(raw.has_contact)) {
1863
+ issues.push({ path: `${path}.has_contact`, message: `has_contact must be one of ${SEQUENCE_CONTACT_CHANNELS.join(", ")}.` });
1864
+ return undefined;
1865
+ }
1866
+ return { has_contact: raw.has_contact, present: raw.present === false ? false : true };
1867
+ }
1813
1868
  if (Array.isArray(raw.all)) {
1814
1869
  return { all: normalizeConditionList(raw.all, `${path}.all`, issues, depth) };
1815
1870
  }
@@ -1820,7 +1875,7 @@ function normalizeCondition(raw, path, issues, depth = 0) {
1820
1875
  const inner = normalizeCondition(raw.not, `${path}.not`, issues, depth + 1);
1821
1876
  return inner ? { not: inner } : undefined;
1822
1877
  }
1823
- issues.push({ path, message: "condition must be {signal}, {has_column}, {all}, {any}, or {not}." });
1878
+ issues.push({ path, message: "condition must be {signal}, {has_column}, {has_contact}, {all}, {any}, or {not}." });
1824
1879
  return undefined;
1825
1880
  }
1826
1881
  function normalizeConditionList(raw, path, issues, depth) {
@@ -2103,9 +2158,10 @@ export function sequenceTemplateVariables(definition) {
2103
2158
  * `rowValues` is optional and backward-compatible: when omitted (undefined), a data
2104
2159
  * leaf ({ has_column }) evaluates as ABSENT — present:false matches, present:true
2105
2160
  * does not — so an existing signal-only caller is unaffected. Signal leaves ignore
2106
- * rowValues entirely.
2161
+ * rowValues entirely. A contact leaf ({ has_contact }) reads `contacts`; without
2162
+ * it, the contact counts as absent.
2107
2163
  */
2108
- export function evaluateSequenceCondition(condition, firedSignals, rowValues) {
2164
+ export function evaluateSequenceCondition(condition, firedSignals, rowValues, contacts) {
2109
2165
  const fired = firedSignals instanceof Set ? firedSignals : new Set(firedSignals);
2110
2166
  if ("signal" in condition) {
2111
2167
  const present = fired.has(condition.signal);
@@ -2115,11 +2171,15 @@ export function evaluateSequenceCondition(condition, firedSignals, rowValues) {
2115
2171
  const present = rowValueIsPresent(rowValues?.[condition.has_column]);
2116
2172
  return condition.present === false ? !present : present;
2117
2173
  }
2174
+ if ("has_contact" in condition) {
2175
+ const present = contacts?.[condition.has_contact] === true;
2176
+ return condition.present === false ? !present : present;
2177
+ }
2118
2178
  if ("all" in condition)
2119
- return condition.all.every((c) => evaluateSequenceCondition(c, fired, rowValues));
2179
+ return condition.all.every((c) => evaluateSequenceCondition(c, fired, rowValues, contacts));
2120
2180
  if ("any" in condition)
2121
- return condition.any.some((c) => evaluateSequenceCondition(c, fired, rowValues));
2122
- return !evaluateSequenceCondition(condition.not, fired, rowValues);
2181
+ return condition.any.some((c) => evaluateSequenceCondition(c, fired, rowValues, contacts));
2182
+ return !evaluateSequenceCondition(condition.not, fired, rowValues, contacts);
2123
2183
  }
2124
2184
  /**
2125
2185
  * Whether a lead's row_values value counts as "present" for a data-leaf branch: a
@@ -29,6 +29,8 @@ export declare function teammatesAvailableTo(env: Record<string, string | undefi
29
29
  export declare const DEFAULT_TEAMMATE_SLUG = "workspace";
30
30
  /** What an Oxygent is called until someone names it, as Grok Bot's are (Tim, 2026-10-13). */
31
31
  export declare const NEW_TEAMMATE_NAME = "New Bot";
32
+ /** What the first Oxygent is called until someone renames it (Philipp, 2026-10-19). */
33
+ export declare const DEFAULT_TEAMMATE_NAME = "Oxygent";
32
34
  export declare const TEAMMATE_NAME_MAX_CHARS = 80;
33
35
  export declare const TEAMMATE_DESCRIPTION_MAX_CHARS = 500;
34
36
  /**
@@ -79,7 +81,7 @@ export declare function teammateDefaultLook(slug: string): {
79
81
  };
80
82
  /**
81
83
  * How an Oxygent presents: its name, what it does and its look. The first
82
- * Oxygent is "New Bot" until its owner names it; nothing renames the stored
84
+ * Oxygent is "Oxygent" until its owner names it; nothing renames the stored
83
85
  * Agent to get there, and the Workspace Agent's stock description is no job.
84
86
  */
85
87
  export declare function teammatePresentation(agent: {
@@ -30,6 +30,8 @@ export function teammatesAvailableTo(env, viewer) {
30
30
  export const DEFAULT_TEAMMATE_SLUG = "workspace";
31
31
  /** What an Oxygent is called until someone names it, as Grok Bot's are (Tim, 2026-10-13). */
32
32
  export const NEW_TEAMMATE_NAME = "New Bot";
33
+ /** What the first Oxygent is called until someone renames it (Philipp, 2026-10-19). */
34
+ export const DEFAULT_TEAMMATE_NAME = "Oxygent";
33
35
  const LEGACY_WORKSPACE_AGENT_NAME = "Workspace Agent";
34
36
  const LEGACY_WORKSPACE_AGENT_DESCRIPTION = "General organization-grounded workspace worker.";
35
37
  export const TEAMMATE_NAME_MAX_CHARS = 80;
@@ -103,12 +105,12 @@ export function teammateDefaultLook(slug) {
103
105
  }
104
106
  /**
105
107
  * How an Oxygent presents: its name, what it does and its look. The first
106
- * Oxygent is "New Bot" until its owner names it; nothing renames the stored
108
+ * Oxygent is "Oxygent" until its owner names it; nothing renames the stored
107
109
  * Agent to get there, and the Workspace Agent's stock description is no job.
108
110
  */
109
111
  export function teammatePresentation(agent) {
110
112
  const first = agent.slug === DEFAULT_TEAMMATE_SLUG;
111
- const name = first && agent.name === LEGACY_WORKSPACE_AGENT_NAME ? NEW_TEAMMATE_NAME : agent.name;
113
+ const name = first && agent.name === LEGACY_WORKSPACE_AGENT_NAME ? DEFAULT_TEAMMATE_NAME : agent.name;
112
114
  const stored = agent.description?.trim() ?? "";
113
115
  const description = first && stored === LEGACY_WORKSPACE_AGENT_DESCRIPTION ? "" : stored;
114
116
  const look = teammateDefaultLook(agent.slug);