@oxygen-agent/cli 1.1011.649 → 1.1011.677
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-manifest.js +4 -0
- package/dist/functions-commands.js +1 -1
- package/dist/help.js +5 -2
- package/dist/index.js +27 -15
- package/dist/skills.js +188 -12
- package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +11 -1
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +50 -26
- package/node_modules/@oxygen/shared/dist/contracts/sender-limits.js +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.d.ts +5 -11
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.d.ts +3 -7
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.generated.js +6 -10
- package/node_modules/@oxygen/shared/dist/copilot-playbooks.js +5 -8
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/command-manifest.js
CHANGED
|
@@ -112,6 +112,10 @@ const MUTATING_COMMANDS = new Set([
|
|
|
112
112
|
"viewers enrichment",
|
|
113
113
|
// A noun leaf that switches a bought domain's yearly paid renewal on or off.
|
|
114
114
|
"domains renewal",
|
|
115
|
+
// The standup reads AND moves the user's Home "last seen" marker, so the next
|
|
116
|
+
// standup, on the web too, starts from this call. An agent assessing a
|
|
117
|
+
// workspace must not mistake it for a pure read (2026-10-04 blind acceptance).
|
|
118
|
+
"home",
|
|
115
119
|
]);
|
|
116
120
|
// `--approved` usually identifies a paid or externally mutating execution
|
|
117
121
|
// mode, but a small number of approved operations are explicitly
|
|
@@ -5,7 +5,7 @@ import { requestOxygen } from "./http-client.js";
|
|
|
5
5
|
export function registerFunctionsCommands(program, handle) {
|
|
6
6
|
const functions = program.command("functions")
|
|
7
7
|
.description("Create, publish and reuse your workspace's Functions backed by ordinary Tables.")
|
|
8
|
-
.addHelpText("after", "\n`functions list` shows your workspace's drafts and published Functions with per-row credit estimates. Attaching (`functions bind`) is free; only `columns run` spends, after a dry run. Use `functions duplicate <function>` to start an editable copy.\nDraft edits are isolated. Publishing updates future invocations of every caller; queued, in-flight and past runs retain their captured version. Existing caller caps never increase automatically.\nFlow: draft → describe → publish → bind → columns run --dry-run → approved run → runs.\nA bound Function executes on its own published execution table and writes named outputs back to the calling table. The call stays visible from the caller: `table-runs list --table <caller> --status all` lists the run, and the binding column's cell holds the outcome (status, outputs, credits, invocation and run ids). Address a Function by id, slug, or the display name shown by `functions list`.\nWorked example with commands: `oxygen skills
|
|
8
|
+
.addHelpText("after", "\n`functions list` shows your workspace's drafts and published Functions with per-row credit estimates. Attaching (`functions bind`) is free; only `columns run` spends, after a dry run. Use `functions duplicate <function>` to start an editable copy.\nDraft edits are isolated. Publishing updates future invocations of every caller; queued, in-flight and past runs retain their captured version. Existing caller caps never increase automatically.\nFlow: draft → describe → publish → bind → columns run --dry-run → approved run → runs.\nA bound Function executes on its own published execution table and writes named outputs back to the calling table. The call stays visible from the caller: `table-runs list --table <caller> --status all` lists the run, and the binding column's cell holds the outcome (status, outputs, credits, invocation and run ids). Address a Function by id, slug, or the display name shown by `functions list`.\nWorked example with commands: `oxygen skills get oxygen-tables --file references/functions.md --json`.\n");
|
|
9
9
|
functions.command("list").description("List your workspace's drafts and published Functions with per-row credit estimates (up to 500).")
|
|
10
10
|
.option("--managed", "Retired compatibility filter; returns an empty list. Use the workspace library without this flag.")
|
|
11
11
|
.option("--favorites", "Only the Functions you favourited (`functions favorite <function>`).")
|
package/dist/help.js
CHANGED
|
@@ -146,10 +146,13 @@ export function applyOxygenHelp(program, binaryName) {
|
|
|
146
146
|
// first-touch surface names the skill that teaches the GTM loops.
|
|
147
147
|
` 2. ${binaryName} skills install --json load the skills that teach the GTM loops (automatic after login)`,
|
|
148
148
|
" Add --project to keep the skills inside the current project, including confined agent sessions.",
|
|
149
|
-
` 3. ${binaryName} context resolve --json
|
|
149
|
+
` 3. ${binaryName} context resolve --purpose onboarding --json load workspace context and the next reads`,
|
|
150
150
|
// Populated workspaces need the same first-win guidance as empty ones.
|
|
151
|
-
` Getting started
|
|
151
|
+
` Getting started? ${binaryName} skills get oxygen-onboarding --json`,
|
|
152
152
|
" It sharpens your offer and ICP, then ranks the warmest leads you can reach now.",
|
|
153
|
+
// A diagnosis is the consultant's (2026-10-04 blind acceptance: help sent
|
|
154
|
+
// "choosing what to do next" to onboarding, so a flat pipeline got no method).
|
|
155
|
+
` Pipeline flat, or planning your GTM? ${binaryName} skills get oxygen-gtm-consultant --json`,
|
|
153
156
|
` 4. ${binaryName} capabilities search "<goal>" --json route the goal, then hydrate one exact command`,
|
|
154
157
|
` ${binaryName} commands search "<term>" --json find a command by name or purpose instead of dumping the manifest`,
|
|
155
158
|
` 5. ${binaryName} recipes list "<goal>" --view summary --json choose a play from the Recipe catalog for your goal`,
|
package/dist/index.js
CHANGED
|
@@ -3780,7 +3780,7 @@ export function createProgram() {
|
|
|
3780
3780
|
});
|
|
3781
3781
|
program
|
|
3782
3782
|
.command("home")
|
|
3783
|
-
.description("Your workspace standup: what happened since you last looked, what needs you, and a year of workspace activity.
|
|
3783
|
+
.description("Your workspace standup: what happened since you last looked, what needs you, and a year of workspace activity. 0 credits. It marks Home as seen, so the next standup (here or on the web) counts from now. The same neutral composition the web Home renders, so a terminal-first operator is not sent to the browser for a status check.")
|
|
3784
3784
|
.option("--json", "Print a JSON envelope.")
|
|
3785
3785
|
.action(async (options) => {
|
|
3786
3786
|
await handleAsyncAction("home standup", options, readHomeStandup);
|
|
@@ -5359,7 +5359,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
5359
5359
|
body: { cpm: parseJsonObject(options.cpmJson ?? "") },
|
|
5360
5360
|
}));
|
|
5361
5361
|
}))
|
|
5362
|
-
.addHelpText("after", "\nExamples (stored snapshots, 0 credits, no provider call):\n oxygen publishing analytics summary --channel linkedin --range 30d --sort impressions --json\n oxygen publishing analytics timeseries --channel linkedin --range 30d --tz Europe/Berlin --json\n\nTotals, leader and trends combine every posting account; `accounts` lists them and `--account <key>` counts only the ones you name. `top_posts[].account` names each post's author.\nWhy a metric is null: every null carries a reason in `null_reasons`:\n not_exposed_by_provider the rail OXYGEN currently reads through never returns it (`capabilities.metrics.<metric>.reason` names the limit)\n not_synced_yet no background snapshot has landed yet\n not_returned_by_provider snapshots exist but the provider left it out (LinkedIn shows impressions only to the post's author)\nSee also: the oxygen-
|
|
5362
|
+
.addHelpText("after", "\nExamples (stored snapshots, 0 credits, no provider call):\n oxygen publishing analytics summary --channel linkedin --range 30d --sort impressions --json\n oxygen publishing analytics timeseries --channel linkedin --range 30d --tz Europe/Berlin --json\n\nTotals, leader and trends combine every posting account; `accounts` lists them and `--account <key>` counts only the ones you name. `top_posts[].account` names each post's author.\nWhy a metric is null: every null carries a reason in `null_reasons`:\n not_exposed_by_provider the rail OXYGEN currently reads through never returns it (`capabilities.metrics.<metric>.reason` names the limit)\n not_synced_yet no background snapshot has landed yet\n not_returned_by_provider snapshots exist but the provider left it out (LinkedIn shows impressions only to the post's author)\nSee also: the oxygen-publishing product skill (`oxygen skills get oxygen-publishing --file references/analytics.md`) for reading LinkedIn performance.\n"))
|
|
5363
5363
|
.addCommand(new Command("import")
|
|
5364
5364
|
.description("Bulk-load scheduled posts from a .csv or .json file. Dry-run by DEFAULT: it parses, lints, and reports invalid rows while writing nothing. Pass --approved to actually write. Imported posts land as drafts that still need approval — import never approves and never publishes. A row whose idempotency_key was already used is skipped, so re-importing the same file cannot duplicate the queue.")
|
|
5365
5365
|
.requiredOption("--file <path>", "Path to a .csv (header row) or .json file ([rows] or { rows: [...] }). Columns: content_text, publish_at, and optionally sender_account_id, provider, timezone, title, first_comment, tags, idempotency_key. A publish_at without Z or an offset is wall-clock time in the row's timezone, else its sender's, else UTC.")
|
|
@@ -9472,7 +9472,7 @@ Examples:
|
|
|
9472
9472
|
].join("\n"));
|
|
9473
9473
|
program
|
|
9474
9474
|
.command("recipes")
|
|
9475
|
-
.description("
|
|
9475
|
+
.description("GTM plays: proven motions with prerequisites, credit posture, and approval gates spelled out, to borrow from and adapt. To design a motion for your company, use the oxygen-gtm-consultant skill: oxygen skills get oxygen-gtm-consultant --json.")
|
|
9476
9476
|
.addCommand(new Command("list")
|
|
9477
9477
|
.description("Find a GTM play for a goal: recipes ranked by relevance to your words; --view summary for a compact index. Filter by business-case category, journey stage, or audience.")
|
|
9478
9478
|
.argument("[query]", "Your goal in plain words, e.g. \"more pipeline from linkedin\" (ranked against title, trigger phrases, tags, and business case).")
|
|
@@ -9651,9 +9651,9 @@ Examples:
|
|
|
9651
9651
|
}));
|
|
9652
9652
|
program
|
|
9653
9653
|
.command("columns")
|
|
9654
|
-
.description("Workspace table column commands. Price first: `tools search --for-table <table>` lists every enrichment this table's columns support with its credit cost per row (0 credits). Ready-made templates (pricing pages, job posts, event pages, 10-Ks, website facts, market questions, text extraction, plus free 0-credit cleanups — company names, job titles, email type, text to list; person questions over an enriched LinkedIn profile and person appearances; company research questions — founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ and footprint, LinkedIn/Crunchbase/careers page lookups): `columns catalog`, then `columns add <table> --prompt-key <key> --input <name>=<column>`. Column choice
|
|
9654
|
+
.description("Workspace table column commands. Price first: `tools search --for-table <table>` lists every enrichment this table's columns support with its credit cost per row (0 credits). Ready-made templates (pricing pages, job posts, event pages, 10-Ks, website facts, market questions, text extraction, plus free 0-credit cleanups — company names, job titles, email type, text to list; person questions over an enriched LinkedIn profile and person appearances; company research questions — founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ and footprint, LinkedIn/Crunchbase/careers page lookups): `columns catalog`, then `columns add <table> --prompt-key <key> --input <name>=<column>`. Column choice guide: the oxygen-tables skill (`oxygen skills get oxygen-tables --json`); enrichment presets and waterfalls: the oxygen-enrichment skill. With --json parse stdout only: the envelope is stdout, and the spend and cost notes (`estimated N credits for a live run`) are stderr, so merging the streams breaks the JSON.")
|
|
9655
9655
|
.addCommand(new Command("add")
|
|
9656
|
-
.description("Add a nullable column to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost. For a page, posting or filing the row already links to, or a company fact Oxygen already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, a LinkedIn or Crunchbase page), pick a template from `columns catalog` (--prompt-key) instead of writing a prompt; or read a URL column with --kind research --research-url. The guide is the oxygen-
|
|
9656
|
+
.description("Add a nullable column to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost. For a page, posting or filing the row already links to, or a company fact Oxygen already knows how to research (founders, parent company, funding, cloud provider, offers demos, industry, NAICS, HQ, a LinkedIn or Crunchbase page), pick a template from `columns catalog` (--prompt-key) instead of writing a prompt; or read a URL column with --kind research --research-url. The guide is the oxygen-tables skill (`oxygen skills get oxygen-tables --json`). For ICP fit, use Classify Score: `columns add <table> --label 'ICP fit' --score 'low,medium,high' --prompt 'Assess {{company_name}}: {{description}}, {{employees}} employees' --grounding workspace` (0.075 credits/row). A formula column needs no run at all: it evaluates on read from its current inputs, so `tables query` shows its values immediately.")
|
|
9657
9657
|
.argument("<table>", "Table id or slug.")
|
|
9658
9658
|
.option("--preset <preset>", "Add a pre-built column bundle instead of one column, which is what --prompt-key adds: a preset writes SEVERAL columns at once and needs no --input on a table whose identity column it can find, while --prompt-key adds one template column and binds each input you name. Presets: `person_enrich` (one LinkedIn profile lookup — Blitz first, the LinkedIn Scraper only when Blitz has no match — then first/last/full name, current company and its LinkedIn URL, job title, start date, education, school, headline, bio, location and followers as free stored fields that fill when the lookup runs; the person templates in `columns catalog --category people` all read its payload column), `company_enrich` (one company lookup from a domain or LinkedIn URL — Blitz first, the LinkedIn Scraper only when Blitz has no match, about 1 credit a company — keeping the whole profile in its JSON column plus name, domain, LinkedIn URL, headcount, industry, description, founded year and HQ country as free stored fields; no field choices; binds to the person preset's current_company_linkedin_url automatically), `company_tech_stack` (the technologies a company runs, from its website and hiring), `company_funding` (total raised, latest round, number of rounds and investors), `company_revenue` (an annual revenue estimate), `company_hiring` (the roles a company is hiring for, with a count) — each of these four is one paid lookup column that fills its own columns, priced per row in `tools search --for-table` — or `domain_check` (1 credit/row for a live/redirect/parked/not_found/dead verdict on every domain, cached 30 days, plus a free label column to filter on). For a single cleaned or classified column, use a template from `columns catalog` instead — that is one column, not a bundle. Name the input column with --input: `url` for person_enrich, `domain` and/or `linkedin_url` for the company presets. Any column works whatever it is called; without --input Oxygen guesses from column names. Creating the columns is free; run them afterwards, --dry-run first.")
|
|
9659
9659
|
.option("--input <slot=column...>", "Bind a preset or template input to an exact column, e.g. --input url=pricing_url with --prompt-key, or --input domain=website --input linkedin_url=company_li with --preset. Repeatable. For a preset it is only needed when the automatic match is wrong or missing; for a template it names the column each declared input reads.", collectRepeatable, [])
|
|
@@ -11055,14 +11055,26 @@ Examples:
|
|
|
11055
11055
|
await handleAsyncAction("people search filters", options, () => requestOxygen("/api/cli/people/search/preview"));
|
|
11056
11056
|
}))
|
|
11057
11057
|
.addCommand(new Command("preview")
|
|
11058
|
-
.description("Preview up to 50 people free, using native filters from people search filters. Then people search run --source people_search --source-filters-json <same filters> for a quoted, approved table fill
|
|
11058
|
+
.description("Preview up to 50 people free, using native filters from people search filters. To see the people at specific companies for free, put their LinkedIn company URLs in company.linkedin_url. Then people search run --source people_search --source-filters-json <same filters> for a quoted, approved table fill; people search run --from-table instead runs one paid search per company in a table and covers each one.")
|
|
11059
11059
|
.requiredOption("--filters-json <json-or-file>", 'Nested native filters as JSON or @file/path, e.g. {"people":{"job_title":{"include":["VP Sales"]}},"company":{"linkedin_url":["https://linkedin.com/company/acme"]}}; {} searches all.')
|
|
11060
|
+
.option("--from-table <table>", "Free sample of Find decision makers at companies: search the first 50 companies in this table in one free page. Pair with --linkedin-url-column.")
|
|
11061
|
+
.option("--linkedin-url-column <key>", "Column of --from-table that holds each company's LinkedIn URL.")
|
|
11060
11062
|
.option("--json", "Print a JSON envelope.")
|
|
11061
11063
|
.action(async (options) => {
|
|
11062
|
-
await handleAsyncAction("people search preview", options, () =>
|
|
11063
|
-
|
|
11064
|
-
|
|
11065
|
-
|
|
11064
|
+
await handleAsyncAction("people search preview", options, async () => {
|
|
11065
|
+
if ((options.fromTable === undefined) !== (options.linkedinUrlColumn === undefined)) {
|
|
11066
|
+
throw new OxygenError("invalid_request", "--from-table and --linkedin-url-column go together.", { exitCode: 1 });
|
|
11067
|
+
}
|
|
11068
|
+
return requestOxygen("/api/cli/people/search/preview", {
|
|
11069
|
+
method: "POST",
|
|
11070
|
+
body: {
|
|
11071
|
+
filters: parseJsonObject(readFileIfPresent(options.filtersJson)),
|
|
11072
|
+
...(options.fromTable !== undefined
|
|
11073
|
+
? { source: { table: options.fromTable, linkedin_url_column: options.linkedinUrlColumn } }
|
|
11074
|
+
: {}),
|
|
11075
|
+
},
|
|
11076
|
+
});
|
|
11077
|
+
});
|
|
11066
11078
|
}))
|
|
11067
11079
|
.addCommand(new Command("plan")
|
|
11068
11080
|
.description("Compile a people-search prompt and optional typed persona filters into ordered provider routes without provider calls.")
|
|
@@ -13232,7 +13244,7 @@ Examples:
|
|
|
13232
13244
|
.command("verify")
|
|
13233
13245
|
.description("Check whether emails you already have are safe to send to. dry_run previews the provider chain for free; live spends and needs --max-credits.")
|
|
13234
13246
|
.addCommand(new Command("email")
|
|
13235
|
-
.description("Verify one or more email addresses. MillionVerifier answers first; eligible catch-all (accept-all) addresses escalate to BounceBan when available. Returns valid / invalid / catch_all / unknown per address. If escalation cannot run, catch_all stays unconfirmed; escalation_unavailable_reason and next_action explain why. On the managed lane, Oxygen staff own dry provider balances, credential repair and hold release; workspace credits do not fix those blockers. A free preview reports an availability snapshot, not guaranteed live readiness. See https://oxygen-agent.com/docs/providers/email-verification and the oxygen-
|
|
13247
|
+
.description("Verify one or more email addresses. MillionVerifier answers first; eligible catch-all (accept-all) addresses escalate to BounceBan when available. Returns valid / invalid / catch_all / unknown per address. If escalation cannot run, catch_all stays unconfirmed; escalation_unavailable_reason and next_action explain why. On the managed lane, Oxygen staff own dry provider balances, credential repair and hold release; workspace credits do not fix those blockers. A free preview reports an availability snapshot, not guaranteed live readiness. See https://oxygen-agent.com/docs/providers/email-verification and the oxygen-enrichment skill (`oxygen skills get oxygen-enrichment --file references/verification.md --json`).")
|
|
13236
13248
|
.argument("<emails...>", "One or more email addresses to verify.")
|
|
13237
13249
|
.option("--mode <mode>", "dry_run (default) previews the plan and spends nothing; live spends credits and returns the answer.")
|
|
13238
13250
|
.option("--approved", "Same as --mode live. Accepted because the global help footer names --approved as the way to authorize a credit-spending command.")
|
|
@@ -15876,11 +15888,11 @@ Examples:
|
|
|
15876
15888
|
}, formatSequenceAnalyticsHealth);
|
|
15877
15889
|
}))
|
|
15878
15890
|
.addCommand(new Command("create")
|
|
15879
|
-
.description("Create a draft multichannel sequence from a steps JSON file. Supports LinkedIn, email, WhatsApp, human call_task, and connected-CRM crm_task journeys. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys). Native email sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport; optional --email-* flags bind an Instantly campaign. Install/read the oxygen-sequencer skill for per-step senders, the launch
|
|
15891
|
+
.description("Create a draft multichannel sequence from a steps JSON file. Supports LinkedIn, email, WhatsApp, human call_task, and connected-CRM crm_task journeys. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys). Native email sends through connected Google/Microsoft mailboxes, including Microsoft SMTP transport; optional --email-* flags bind an Instantly campaign. Install/read the oxygen-sequencer skill for per-step senders (references/email.md, references/linkedin.md), the launch ladder and complete mapped CRM task examples (references/calls-and-crm-tasks.md).")
|
|
15880
15892
|
.addHelpText("after", "\nSenders: each new lead gets one of the --sender-profiles people, the one with the fewest of this sequence's leads (a lead already talking to one of your LinkedIn accounts keeps that account's person), and its LinkedIn and email steps come from that person (email from one of their inboxes). Per step, in the steps file: email_send.sender_profile_id sends that email from any inbox of one selected person (the normal inbox rotation spreads leads across that person's inboxes and each lead keeps its one; a lead already assigned to that person keeps its own inbox; with none usable the lead waits, never using a colleague's inbox), and email_send.sender_mailbox_id from one exact inbox (set one or neither); a LinkedIn step's sender_profile_id sends it from one selected person. Invite, message, inmail and withdraw_invite steps must share one sender_profile_id or none: that person then owns every lead's LinkedIn thread (LinkedIn allows one of your accounts per conversation). visit_profile, follow, like_post and comment_post may each name anyone selected. An email whose From changes, including the return to the lead's inbox, needs its own subject_template. To check who sends each step, run `sequences start <sequence>` without --approved; nothing changes.\n")
|
|
15881
15893
|
.requiredOption("--name <name>", "Human-readable sequence name.")
|
|
15882
15894
|
.requiredOption("--slug <slug>", "Unique slug for the sequence.")
|
|
15883
|
-
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (visit_profile | invite | wait_for_connection | message | inmail | follow | like_post | comment_post | withdraw_invite), email steps (email_send | email_enroll | email_move | email_stop), WhatsApp (whatsapp_message), human call tasks (call_task), connected-CRM tasks (crm_task; channel crm; exact identity record_mappings + explicit create/update policy + record_links + dynamic task property_mappings/associations), and control steps (wait | wait_for_signal | branch | stop), each with an `id`. Copy templates support {{column}} interpolation from the lead's row, {{column|fallback}} inline fallbacks, deterministic spintax — {{RANDOM|a|b|c}} or industry-standard bare {Hi|Hey|Hello}, nestable like {Would {Tuesday|Thursday} work|next week?} — and {% if column %}…{% endif %} conditionals; a bare {…} region is spintax only when it contains a top-level |, so literal braces (CSS/JSON) pass through. Every step's copy can also use {{sender_first_name}}, {{sender_last_name}} and {{sender_full_name}} (alias {{sender_name}}): the name of the person who sends that step (their sender profile, else the account or inbox display name), with no table column needed; email steps add {{sender_email}} (the from address). A row column of the same name WINS on collision, and a missing name renders empty. comment_post takes text_template and/or ai_prompt — a KG-grounded comment generated at send time (a paid AI call) that falls back to text_template if generation fails. A `branch` routes on signals (then/else) or the legacy connection_accepted/already_connected/open_profile sugar (then_id/else_id). A signal condition can also branch on the LEAD'S DATA: a data leaf { has_column: \"email\" } is true when that row_values column has a non-empty value (add present:false for \"missing\") — e.g. route leads that have an email down an email arm and the rest down a LinkedIn arm. MINIMAL EMAIL STEP SHAPE: { \"id\": \"s1\", \"channel\": \"email\", \"kind\": \"email_send\", \"subject_template\": \"...\", \"body_template\": \"...\" } — body_template is REQUIRED on email_send; subject_template is OPTIONAL and is the one control that decides threading: give a step its own subject and it goes out as a NEW email, omit it and the step continues the lead's previous email in the same thread, going out as \"Re: <the previous email's subject>\" (what the retired email_reply kind used to be — still accepted on input, rewritten into this shape, and reported under `warnings` in the response). The FIRST email step of a sequence must carry a subject, since it has no earlier thread to continue. MINIMAL WHATSAPP STEP SHAPE: { \"id\": \"s1\", \"channel\": \"whatsapp\", \"kind\": \"whatsapp_message\", \"template\": \"Hi {{first_name|there}} …\" } — template is REQUIRED, and a live WhatsApp start also needs the sequence's --whatsapp-cold-initiate opt-in. MINIMAL WAIT STEP SHAPE: { \"id\": \"w1\", \"kind\": \"wait\", \"days\": 3 } — days and/or hours, at least 1 hour in total. A/B tests use an explicit `variants` array of copy partials on the step (up to 25 alternates, a–z; spintax varies wording INSIDE one variant and is not A/B-tracked). For the complete replay-safe crm_task upsert mapping shape,
|
|
15895
|
+
.requiredOption("--steps-file <path>", "Path to a JSON file: { \"steps\": [...] }. LinkedIn steps (visit_profile | invite | wait_for_connection | message | inmail | follow | like_post | comment_post | withdraw_invite), email steps (email_send | email_enroll | email_move | email_stop), WhatsApp (whatsapp_message), human call tasks (call_task), connected-CRM tasks (crm_task; channel crm; exact identity record_mappings + explicit create/update policy + record_links + dynamic task property_mappings/associations), and control steps (wait | wait_for_signal | branch | stop), each with an `id`. Copy templates support {{column}} interpolation from the lead's row, {{column|fallback}} inline fallbacks, deterministic spintax — {{RANDOM|a|b|c}} or industry-standard bare {Hi|Hey|Hello}, nestable like {Would {Tuesday|Thursday} work|next week?} — and {% if column %}…{% endif %} conditionals; a bare {…} region is spintax only when it contains a top-level |, so literal braces (CSS/JSON) pass through. Every step's copy can also use {{sender_first_name}}, {{sender_last_name}} and {{sender_full_name}} (alias {{sender_name}}): the name of the person who sends that step (their sender profile, else the account or inbox display name), with no table column needed; email steps add {{sender_email}} (the from address). A row column of the same name WINS on collision, and a missing name renders empty. comment_post takes text_template and/or ai_prompt — a KG-grounded comment generated at send time (a paid AI call) that falls back to text_template if generation fails. A `branch` routes on signals (then/else) or the legacy connection_accepted/already_connected/open_profile sugar (then_id/else_id). A signal condition can also branch on the LEAD'S DATA: a data leaf { has_column: \"email\" } is true when that row_values column has a non-empty value (add present:false for \"missing\") — e.g. route leads that have an email down an email arm and the rest down a LinkedIn arm. MINIMAL EMAIL STEP SHAPE: { \"id\": \"s1\", \"channel\": \"email\", \"kind\": \"email_send\", \"subject_template\": \"...\", \"body_template\": \"...\" } — body_template is REQUIRED on email_send; subject_template is OPTIONAL and is the one control that decides threading: give a step its own subject and it goes out as a NEW email, omit it and the step continues the lead's previous email in the same thread, going out as \"Re: <the previous email's subject>\" (what the retired email_reply kind used to be — still accepted on input, rewritten into this shape, and reported under `warnings` in the response). The FIRST email step of a sequence must carry a subject, since it has no earlier thread to continue. MINIMAL WHATSAPP STEP SHAPE: { \"id\": \"s1\", \"channel\": \"whatsapp\", \"kind\": \"whatsapp_message\", \"template\": \"Hi {{first_name|there}} …\" } — template is REQUIRED, and a live WhatsApp start also needs the sequence's --whatsapp-cold-initiate opt-in. MINIMAL WAIT STEP SHAPE: { \"id\": \"w1\", \"kind\": \"wait\", \"days\": 3 } — days and/or hours, at least 1 hour in total. A/B tests use an explicit `variants` array of copy partials on the step (up to 25 alternates, a–z; spintax varies wording INSIDE one variant and is not A/B-tracked). For the complete replay-safe crm_task upsert mapping shape, read `oxygen skills get oxygen-sequencer --file references/calls-and-crm-tasks.md`.")
|
|
15884
15896
|
.option("--channels <list>", "Comma-separated channels: linkedin,email,whatsapp,call,crm. Defaults to the channels the journey touches. crm_task currently supports HubSpot.")
|
|
15885
15897
|
.option("--whatsapp-cold-initiate", "WhatsApp: allow cold-initiating new chats (no prior conversation). Required to start a WhatsApp sequence live — WhatsApp via Unipile is unofficial WhatsApp Web, so cold-initiating is an explicit ban-risk opt-in.")
|
|
15886
15898
|
.option("--phone-column-key <key>", "WhatsApp: row_values key holding each lead's phone number (else falls back to phone/phone_number/mobile).")
|
|
@@ -20032,7 +20044,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
|
|
|
20032
20044
|
await handleAsyncAction("skills doctor", options, () => doctorAgentSkills(options));
|
|
20033
20045
|
}))
|
|
20034
20046
|
.addCommand(new Command("install")
|
|
20035
|
-
.description("Install Oxygen agent skills into local agent skill directories only; uses 0 credits and does not change the Oxygen workspace or call a provider.")
|
|
20047
|
+
.description("Install Oxygen agent skills into local agent skill directories only; uses 0 credits and does not change the Oxygen workspace or call a provider. Also removes retired Oxygen skills it installed earlier.")
|
|
20036
20048
|
.option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
20037
20049
|
.option("--agents <agents...>", "Space or comma separated agents. Defaults to codex, claude-code, and cursor.")
|
|
20038
20050
|
.option("--skill <skill>", "Skill name or '*'. Defaults to '*'.")
|
|
@@ -22860,7 +22872,7 @@ function readPeopleSearchRunBody(options) {
|
|
|
22860
22872
|
// --source people_search is the Find people lane: the free preview's filters
|
|
22861
22873
|
// ARE the request, so no prompt, plan or companies table is synthesized.
|
|
22862
22874
|
if (options.source !== undefined && options.source !== "people_search") {
|
|
22863
|
-
throw new OxygenError("invalid_request", "--source must be people_search; use --from-table
|
|
22875
|
+
throw new OxygenError("invalid_request", "--source must be people_search; use --from-table to find decision makers at the companies in a table.", { exitCode: 1 });
|
|
22864
22876
|
}
|
|
22865
22877
|
if (options.source === "people_search") {
|
|
22866
22878
|
if (!options.sourceFiltersJson) {
|
package/dist/skills.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
|
-
import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { closeSync, existsSync, lstatSync, mkdirSync, mkdtempSync, openSync, readFileSync, readSync, readdirSync, readlinkSync, rmSync, unlinkSync, writeFileSync, } from "node:fs";
|
|
3
3
|
import { homedir, tmpdir } from "node:os";
|
|
4
4
|
import { dirname, join, posix, resolve } from "node:path";
|
|
5
5
|
import { OxygenError, errorMessage, inferUserCapabilityRoute, serializeCapabilityRoute, toFailure, } from "@oxygen/shared";
|
|
@@ -10,6 +10,15 @@ import { resolveCmdShimInvocation } from "./windows-shim.js";
|
|
|
10
10
|
const DEFAULT_SKILL_AGENTS = ["codex", "claude-code", "cursor"];
|
|
11
11
|
const SKILL_INDEX_PATH = "/.well-known/skills/index.json";
|
|
12
12
|
const SKILLS_ACCESS_API_PATH = "/api/cli/skills/access";
|
|
13
|
+
/** Prefix of the private temp directory every install stages skill content in. */
|
|
14
|
+
const STAGING_DIR_PREFIX = "oxygen-skills-";
|
|
15
|
+
/**
|
|
16
|
+
* The footer the web app's `stampAgentSkillProvenance` appends as the last line
|
|
17
|
+
* of every markdown file an authenticated skills grant serves. It is how an
|
|
18
|
+
* install proves a copy on disk came from OXYGEN before it removes one.
|
|
19
|
+
*/
|
|
20
|
+
const OXYGEN_PROVENANCE_FOOTER = /^<!-- oxygen provenance: org=[0-9a-f]+ issued=\d{4}-\d{2}-\d{2} skills=\S+ -->$/;
|
|
21
|
+
const PROVENANCE_TAIL_BYTES = 1024;
|
|
13
22
|
// Display names the vendored `npx skills` installer (1.5.x) uses in its own
|
|
14
23
|
// "Failed to install" console lines (`✗ <skill> → <displayName>: <reason>`).
|
|
15
24
|
// Only used to tell a genuine failure of an agent we actually requested apart
|
|
@@ -223,7 +232,8 @@ options, runtime = {}) {
|
|
|
223
232
|
const platform = runtime.platform ?? process.platform;
|
|
224
233
|
const spawn = runtime.spawnSync ?? wrapExecFileSync(runtime.execFileSync) ?? spawnSync;
|
|
225
234
|
const projectDir = resolveSkillProjectDir(options.project);
|
|
226
|
-
const
|
|
235
|
+
const home = resolveInstallerHome(env, platform);
|
|
236
|
+
const installLocations = resolveSkillInstallLocations(agents, projectDir, home, env);
|
|
227
237
|
// The vendored installer writes immediately once spawned. Tell humans and
|
|
228
238
|
// agents the exact destination first so a default global install is never a
|
|
229
239
|
// surprise, while keeping stdout as one stable JSON envelope under --json.
|
|
@@ -244,12 +254,13 @@ options, runtime = {}) {
|
|
|
244
254
|
// tokenized URLs stay inside this process on the fetches) and hand the
|
|
245
255
|
// installer that local path instead.
|
|
246
256
|
let stagingDir = null;
|
|
257
|
+
let retiredSkills = [];
|
|
247
258
|
const outputs = [];
|
|
248
259
|
const skippedTargets = [];
|
|
249
260
|
try {
|
|
250
261
|
let installSource = source.install_url;
|
|
251
|
-
stagingDir = mkdtempSync(join(tmpdir(),
|
|
252
|
-
await stageAuthenticatedSkills(stagingDir, source, targets, runtime.fetch ?? fetch);
|
|
262
|
+
stagingDir = mkdtempSync(join(tmpdir(), STAGING_DIR_PREFIX));
|
|
263
|
+
retiredSkills = await stageAuthenticatedSkills(stagingDir, source, targets, runtime.fetch ?? fetch);
|
|
253
264
|
installSource = stagingDir;
|
|
254
265
|
for (const target of targets) {
|
|
255
266
|
const args = installerAddArgs(installSource, agents, target);
|
|
@@ -311,9 +322,17 @@ options, runtime = {}) {
|
|
|
311
322
|
// installer swept in beyond what we requested are reported distinctly and
|
|
312
323
|
// never counted as a failure — see classifyInstallerFailures.
|
|
313
324
|
const skippedAgents = [...new Set(skippedTargets.map((entry) => entry.agent))];
|
|
325
|
+
// A copy of a skill OXYGEN retired keeps loading withdrawn instructions until
|
|
326
|
+
// it is removed, including the installer's canonical copy that agent skill
|
|
327
|
+
// directories symlink to.
|
|
328
|
+
const retired = removeRetiredAgentSkills(retiredSkills, installerCanonicalSkillsDir(projectDir, home), installLocations.map((location) => location.path), projectDir);
|
|
314
329
|
if (dependencySkills.length > 0) {
|
|
315
330
|
process.stderr.write(`note: '${skill}' also installs its prerequisite skill(s): ${dependencySkills.join(", ")}.\n`);
|
|
316
331
|
}
|
|
332
|
+
if (retired.removed.length > 0) {
|
|
333
|
+
const lockFiles = [...new Set(retired.removed.flatMap((entry) => entry.lock_file ? [entry.lock_file] : []))];
|
|
334
|
+
process.stderr.write(`note: removed retired Oxygen skill(s) installed earlier: ${retired.removed.map((entry) => entry.path).join(", ")}${lockFiles.length > 0 ? `; updated ${lockFiles.join(", ")} to match` : ""}.\n`);
|
|
335
|
+
}
|
|
317
336
|
return {
|
|
318
337
|
indexUrl: source.index_url,
|
|
319
338
|
index_url: source.index_url,
|
|
@@ -335,6 +354,8 @@ options, runtime = {}) {
|
|
|
335
354
|
// Any genuine failure threw above rather than reaching this return.
|
|
336
355
|
failed_count: 0,
|
|
337
356
|
summary: summarizeInstallOutcome(agents.length, skippedAgents.length, 0),
|
|
357
|
+
retired_skills_removed: retired.removed,
|
|
358
|
+
retired_skills_kept: retired.kept,
|
|
338
359
|
};
|
|
339
360
|
}
|
|
340
361
|
/**
|
|
@@ -349,7 +370,7 @@ export function resolveSkillProjectDir(project) {
|
|
|
349
370
|
return resolve(project.trim());
|
|
350
371
|
return null;
|
|
351
372
|
}
|
|
352
|
-
function resolveSkillInstallLocations(agents, projectDir, env) {
|
|
373
|
+
function resolveSkillInstallLocations(agents, projectDir, home, env) {
|
|
353
374
|
const project = projectDir !== null;
|
|
354
375
|
const base = projectDir ?? process.cwd();
|
|
355
376
|
const projectRoots = {
|
|
@@ -358,16 +379,165 @@ function resolveSkillInstallLocations(agents, projectDir, env) {
|
|
|
358
379
|
cursor: resolve(base, ".agents", "skills"),
|
|
359
380
|
};
|
|
360
381
|
const globalRoots = {
|
|
361
|
-
codex: join(env.CODEX_HOME?.trim() || join(
|
|
362
|
-
"claude-code": join(env.CLAUDE_CONFIG_DIR?.trim() || join(
|
|
363
|
-
cursor: join(
|
|
382
|
+
codex: join(env.CODEX_HOME?.trim() || join(home, ".codex"), "skills"),
|
|
383
|
+
"claude-code": join(env.CLAUDE_CONFIG_DIR?.trim() || join(home, ".claude"), "skills"),
|
|
384
|
+
cursor: join(home, ".cursor", "skills"),
|
|
364
385
|
};
|
|
365
386
|
const roots = project ? projectRoots : globalRoots;
|
|
366
387
|
return agents.map((agent) => ({
|
|
367
388
|
agent,
|
|
368
|
-
path: roots[agent] ?? (
|
|
389
|
+
path: roots[agent] ?? installerCanonicalSkillsDir(projectDir, home),
|
|
369
390
|
}));
|
|
370
391
|
}
|
|
392
|
+
/**
|
|
393
|
+
* The home directory the spawned installer resolves: it runs with `env`, so its
|
|
394
|
+
* `os.homedir()` reads HOME (USERPROFILE on Windows) from that same environment.
|
|
395
|
+
*/
|
|
396
|
+
function resolveInstallerHome(env, platform) {
|
|
397
|
+
return (platform === "win32" ? env.USERPROFILE : env.HOME)?.trim() || homedir();
|
|
398
|
+
}
|
|
399
|
+
/** Where `npx skills add` keeps the real copy that agent skill directories link to. */
|
|
400
|
+
function installerCanonicalSkillsDir(projectDir, home) {
|
|
401
|
+
return projectDir ? resolve(projectDir, ".agents", "skills") : join(home, ".agents", "skills");
|
|
402
|
+
}
|
|
403
|
+
/**
|
|
404
|
+
* Removes copies of skills OXYGEN retired, so agents stop loading instructions
|
|
405
|
+
* OXYGEN withdrew. A directory is removed only when its SKILL.md ends with
|
|
406
|
+
* OXYGEN's provenance footer. A symlink is never followed: the only one removed
|
|
407
|
+
* is the installer's own link to a canonical copy that is gone, which would
|
|
408
|
+
* otherwise stay behind as a broken skill entry. The canonical directory is
|
|
409
|
+
* visited first, so its copies are decided before the links to them are
|
|
410
|
+
* reached. Anything else found at a retired name is kept with the reason; a
|
|
411
|
+
* project install's canonical removals are also dropped from its installer lock.
|
|
412
|
+
*/
|
|
413
|
+
function removeRetiredAgentSkills(names, canonicalDir, agentDirs, projectDir) {
|
|
414
|
+
const outcome = { removed: [], kept: [] };
|
|
415
|
+
for (const dir of new Set([canonicalDir, ...agentDirs])) {
|
|
416
|
+
for (const name of names) {
|
|
417
|
+
const path = join(dir, name);
|
|
418
|
+
let disposition;
|
|
419
|
+
try {
|
|
420
|
+
disposition = retiredCopyDisposition(path, resolve(canonicalDir, name));
|
|
421
|
+
}
|
|
422
|
+
catch (error) {
|
|
423
|
+
// Nothing is there (ENOTDIR: the skills directory itself is a file).
|
|
424
|
+
if (errorCode(error) === "ENOENT" || errorCode(error) === "ENOTDIR")
|
|
425
|
+
continue;
|
|
426
|
+
disposition = { keep: `unreadable (${errorCode(error)})` };
|
|
427
|
+
}
|
|
428
|
+
if ("remove" in disposition) {
|
|
429
|
+
try {
|
|
430
|
+
// A link is unlinked, never removed recursively.
|
|
431
|
+
if (disposition.remove === "link")
|
|
432
|
+
unlinkSync(path);
|
|
433
|
+
else
|
|
434
|
+
rmSync(path, { recursive: true, force: true });
|
|
435
|
+
}
|
|
436
|
+
catch (error) {
|
|
437
|
+
disposition = { keep: `removal failed (${errorCode(error)})` };
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if ("keep" in disposition) {
|
|
441
|
+
outcome.kept.push({ name, path, reason: disposition.keep });
|
|
442
|
+
continue;
|
|
443
|
+
}
|
|
444
|
+
const lockFile = projectDir && dir === canonicalDir ? dropStagedLockEntry(projectDir, name) : null;
|
|
445
|
+
outcome.removed.push({ name, path, ...(lockFile ? { lock_file: lockFile } : {}) });
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
return outcome;
|
|
449
|
+
}
|
|
450
|
+
/**
|
|
451
|
+
* Whether to keep the entry at a retired skill's name, and why, or how to
|
|
452
|
+
* remove it. Throws when `path` itself cannot be lstat'ed (ENOENT: nothing is
|
|
453
|
+
* there).
|
|
454
|
+
*/
|
|
455
|
+
function retiredCopyDisposition(path, canonicalCopy) {
|
|
456
|
+
const entry = lstatSync(path);
|
|
457
|
+
if (entry.isSymbolicLink())
|
|
458
|
+
return retiredLinkDisposition(path, canonicalCopy);
|
|
459
|
+
if (!entry.isDirectory())
|
|
460
|
+
return { keep: "not a directory" };
|
|
461
|
+
const skillFile = join(path, "SKILL.md");
|
|
462
|
+
try {
|
|
463
|
+
const file = lstatSync(skillFile);
|
|
464
|
+
if (!file.isFile())
|
|
465
|
+
return { keep: "SKILL.md is not a regular file" };
|
|
466
|
+
return OXYGEN_PROVENANCE_FOOTER.test(lastLineOf(skillFile, file.size))
|
|
467
|
+
? { remove: "directory" }
|
|
468
|
+
: { keep: "SKILL.md carries no OXYGEN provenance marker" };
|
|
469
|
+
}
|
|
470
|
+
catch (error) {
|
|
471
|
+
return { keep: errorCode(error) === "ENOENT" ? "no SKILL.md" : `SKILL.md unreadable (${errorCode(error)})` };
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
/**
|
|
475
|
+
* A link is removable only when its target, resolved against the link's own
|
|
476
|
+
* directory, is exactly this skill's canonical copy and nothing is there any
|
|
477
|
+
* more: the installer's own link, broken once this pass or an earlier one
|
|
478
|
+
* removed the copy. The target is never followed, only lstat'ed.
|
|
479
|
+
*/
|
|
480
|
+
function retiredLinkDisposition(link, canonicalCopy) {
|
|
481
|
+
let target;
|
|
482
|
+
try {
|
|
483
|
+
target = resolve(dirname(link), readlinkSync(link));
|
|
484
|
+
}
|
|
485
|
+
catch (error) {
|
|
486
|
+
return { keep: `symlink; unreadable (${errorCode(error)})` };
|
|
487
|
+
}
|
|
488
|
+
if (target !== canonicalCopy)
|
|
489
|
+
return { keep: "symlink to a path outside the installer's canonical copy" };
|
|
490
|
+
try {
|
|
491
|
+
lstatSync(target);
|
|
492
|
+
return { keep: "symlink to the installer's canonical copy, which is still present" };
|
|
493
|
+
}
|
|
494
|
+
catch (error) {
|
|
495
|
+
return errorCode(error) === "ENOENT"
|
|
496
|
+
? { remove: "link" }
|
|
497
|
+
: { keep: `symlink; its target is unreadable (${errorCode(error)})` };
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
/** The last non-empty line of a file, read from its tail only. */
|
|
501
|
+
function lastLineOf(file, size) {
|
|
502
|
+
const length = Math.min(size, PROVENANCE_TAIL_BYTES);
|
|
503
|
+
const tail = Buffer.alloc(length);
|
|
504
|
+
const fd = openSync(file, "r");
|
|
505
|
+
try {
|
|
506
|
+
readSync(fd, tail, 0, length, size - length);
|
|
507
|
+
}
|
|
508
|
+
finally {
|
|
509
|
+
closeSync(fd);
|
|
510
|
+
}
|
|
511
|
+
return tail.toString("utf8").trimEnd().split("\n").pop()?.trim() ?? "";
|
|
512
|
+
}
|
|
513
|
+
/**
|
|
514
|
+
* A project install of a local source is recorded in `<project>/skills-lock.json`
|
|
515
|
+
* (a global one writes no lock entry). Drop a removed skill's entry only when it
|
|
516
|
+
* names an OXYGEN staging directory as its source, so the installer's lock stops
|
|
517
|
+
* listing a skill that is gone. Returns the lock file it changed, if any.
|
|
518
|
+
*/
|
|
519
|
+
function dropStagedLockEntry(projectDir, name) {
|
|
520
|
+
const lockFile = join(projectDir, "skills-lock.json");
|
|
521
|
+
try {
|
|
522
|
+
const lock = JSON.parse(readFileSync(lockFile, "utf8"));
|
|
523
|
+
if (!isRecord(lock) || !isRecord(lock.skills))
|
|
524
|
+
return null;
|
|
525
|
+
const entry = lock.skills[name];
|
|
526
|
+
if (!isRecord(entry) || entry.sourceType !== "local" || typeof entry.source !== "string")
|
|
527
|
+
return null;
|
|
528
|
+
if (!(entry.source.split(/[\\/]/).pop() ?? "").startsWith(STAGING_DIR_PREFIX))
|
|
529
|
+
return null;
|
|
530
|
+
delete lock.skills[name];
|
|
531
|
+
writeFileSync(lockFile, `${JSON.stringify(lock, null, 2)}\n`);
|
|
532
|
+
return lockFile;
|
|
533
|
+
}
|
|
534
|
+
catch {
|
|
535
|
+
return null;
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
function errorCode(error) {
|
|
539
|
+
return error?.code ?? errorMessage(error);
|
|
540
|
+
}
|
|
371
541
|
/**
|
|
372
542
|
* Stages the requested skill(s) into `dir` so the installer receives a local
|
|
373
543
|
* filesystem path instead of the org-bound tokenized access URL. Keeping the URL
|
|
@@ -378,10 +548,10 @@ function resolveSkillInstallLocations(agents, projectDir, env) {
|
|
|
378
548
|
*
|
|
379
549
|
* Any fetch/staging failure throws the same `skills_install_failed` error the
|
|
380
550
|
* installer path raises — we never fall back to passing the tokenized URL as a
|
|
381
|
-
* process argument.
|
|
551
|
+
* process argument. Returns the skill names the index lists as retired.
|
|
382
552
|
*/
|
|
383
553
|
async function stageAuthenticatedSkills(dir, source, targets, fetchImpl) {
|
|
384
|
-
const entries = await fetchStagedSkillEntries(source.index_url, fetchImpl);
|
|
554
|
+
const { entries, retired } = await fetchStagedSkillEntries(source.index_url, fetchImpl);
|
|
385
555
|
const wanted = targets.includes("*")
|
|
386
556
|
? entries
|
|
387
557
|
: entries.filter((entry) => targets.includes(entry.name));
|
|
@@ -418,6 +588,7 @@ async function stageAuthenticatedSkills(dir, source, targets, fetchImpl) {
|
|
|
418
588
|
if (failure !== undefined)
|
|
419
589
|
throw failure;
|
|
420
590
|
}
|
|
591
|
+
return retired;
|
|
421
592
|
}
|
|
422
593
|
const STAGED_FILE_CONCURRENCY = 6;
|
|
423
594
|
const STAGED_FILE_TIMEOUT_MS = 30_000;
|
|
@@ -460,7 +631,12 @@ async function fetchStagedSkillEntries(indexUrl, fetchImpl) {
|
|
|
460
631
|
}
|
|
461
632
|
entries.push({ name: entry.name, files });
|
|
462
633
|
}
|
|
463
|
-
|
|
634
|
+
// Older servers omit `retired`, which removes nothing. A name that is unsafe
|
|
635
|
+
// as a path segment, or that the index still serves, is never treated as one.
|
|
636
|
+
const retired = Array.isArray(parsed.retired)
|
|
637
|
+
? [...new Set(parsed.retired)].filter((name) => typeof name === "string" && isSafeSkillName(name) && !entries.some((entry) => entry.name === name))
|
|
638
|
+
: [];
|
|
639
|
+
return { entries, retired };
|
|
464
640
|
}
|
|
465
641
|
/** A transport failure, a timeout, 429 or 5xx is retried; any other status is final. */
|
|
466
642
|
async function fetchStagedSkillFile(url, fetchImpl) {
|
|
@@ -79,6 +79,16 @@ export declare function inferCapabilityRoute(query: string): CapabilityRouteMatc
|
|
|
79
79
|
*/
|
|
80
80
|
export declare function catalogueFindTool(row: EnrichmentIntentTaxonomyEntry): string | null;
|
|
81
81
|
export declare function inferPrimitiveRoute(query: string): PrimitiveRouteMatch | null;
|
|
82
|
+
/**
|
|
83
|
+
* Skills OXYGEN once served and has retired (2026-10-04: plays are inspiration
|
|
84
|
+
* read through the recipes API, never skills, and visual design is taught by
|
|
85
|
+
* its commands' own help; LinkedIn marketing split into the Publishing and
|
|
86
|
+
* Signals feature skills, with its account-safety rules in the sequencer's
|
|
87
|
+
* LinkedIn reference). The manifest no longer serves them, so nothing new
|
|
88
|
+
* can install, attach or advertise them; their audience below stays so an Agent
|
|
89
|
+
* version or Copilot session that already pinned one keeps its snapshot.
|
|
90
|
+
*/
|
|
91
|
+
export declare const RETIRED_PRODUCT_SKILLS: readonly string[];
|
|
82
92
|
/** Current hosted instruction audiences, shared by discovery and Agent snapshots. */
|
|
83
93
|
export declare const OXYGEN_PRODUCT_SKILL_AUDIENCES: Readonly<Record<string, "workspace" | "agency_partner">>;
|
|
84
94
|
export declare function canReadProductSkillInstructions(name: string, entitlements: {
|
|
@@ -89,7 +99,7 @@ export declare function canReadProductSkillInstructions(name: string, entitlemen
|
|
|
89
99
|
export declare function readProductSkillInstructionResult(capabilityName: string, result: unknown): {
|
|
90
100
|
slug: string | null;
|
|
91
101
|
} | null;
|
|
92
|
-
/** Public action cards never advertise invite-only or unknown skill instructions. */
|
|
102
|
+
/** Public action cards never advertise invite-only, retired or unknown skill instructions. */
|
|
93
103
|
export declare function isPublicCapabilitySkill(name: string): boolean;
|
|
94
104
|
export declare function serializeCapabilityRoute(route: CapabilityRouteMatch | null, surfaceOrOptions?: CapabilityRouteSurface | {
|
|
95
105
|
visibleSkills?: readonly string[];
|
|
@@ -14,7 +14,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
14
14
|
posture: "mixed",
|
|
15
15
|
gatewayTools: ["oxygen_visual_source_create", "oxygen_visual_render_preview", "oxygen_visual_render_get", "oxygen_visual_image_view"],
|
|
16
16
|
gatewayCommands: ["visuals source-create", "visuals render-preview", "visuals render-get", "visuals download"],
|
|
17
|
-
skills: ["oxygen-
|
|
17
|
+
skills: ["oxygen-knowledge"],
|
|
18
18
|
endpointSections: ["visuals"],
|
|
19
19
|
intentTerms: ["infographic", "gtm flow image", "render html", "visual design", "carousel pages", "svg", "png", "pdf export", "graphic designer"],
|
|
20
20
|
},
|
|
@@ -64,13 +64,13 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
64
64
|
id: "onboarding-and-copilot",
|
|
65
65
|
layer: "Control",
|
|
66
66
|
primitive: null,
|
|
67
|
-
owns: "First-run setup and interactive Workspace Copilot sessions over the shared primitive contracts.",
|
|
67
|
+
owns: "First-run setup, GTM diagnosis and planning, and interactive Workspace Copilot sessions over the shared primitive contracts.",
|
|
68
68
|
notFor: "Recurring deterministic automation or standing adaptive jobs; use Workflows or Agents.",
|
|
69
|
-
execution: "Use existing onboarding context to understand the user goal and GTM bottleneck, recommend a useful next improvement, and guide its native implementation when selected.",
|
|
69
|
+
execution: "Use existing onboarding context to understand the user goal and GTM bottleneck, recommend a useful next improvement, and guide its native implementation when selected. For a diagnosis or a GTM plan, assess the workspace, agree the binding constraint, and design a custom motion run as a capped pilot.",
|
|
70
70
|
posture: "mixed",
|
|
71
71
|
gatewayTools: ["oxygen_whoami", "oxygen_recipes_list", "oxygen_blueprints_list", "oxygen_context_resolve", "oxygen_copilot_start"],
|
|
72
72
|
gatewayCommands: ["whoami", "context resolve", "recipes list", "blueprints list", "copilot start"],
|
|
73
|
-
skills: ["oxygen-onboarding", "oxygen-quickstart", "oxygen-gtm"],
|
|
73
|
+
skills: ["oxygen-onboarding", "oxygen-gtm-consultant", "oxygen-quickstart", "oxygen-gtm"],
|
|
74
74
|
// `activation` remains an exact legacy API family for saved clients, but it
|
|
75
75
|
// has no recommended gateway and is not an intent term or first-touch path.
|
|
76
76
|
endpointSections: ["activation", "copilot", "home", "onboarding"],
|
|
@@ -78,7 +78,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
78
78
|
// users say ("next step of this workflow run"), and a 6-point multi-word hit
|
|
79
79
|
// would drag those queries here. The terms kept are the ones only a founder
|
|
80
80
|
// asking where they are in setup actually types.
|
|
81
|
-
intentTerms: ["onboard", "onboarding", "first run", "first day", "setup oxygen", "copilot", "assistant", "guided", "where am i", "next step in setup", "what do i do next", "gtm consultant", "gtm planning", "improve our gtm operation"],
|
|
81
|
+
intentTerms: ["onboard", "onboarding", "first run", "first day", "setup oxygen", "copilot", "assistant", "guided", "where am i", "next step in setup", "what do i do next", "gtm consultant", "gtm planning", "improve our gtm operation", "gtm review", "pipeline is flat", "go-to-market plan"],
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
84
|
id: "billing-and-budgets",
|
|
@@ -196,7 +196,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
196
196
|
posture: "workspace_write",
|
|
197
197
|
gatewayTools: ["oxygen_recipes_list", "oxygen_recipes_get"],
|
|
198
198
|
gatewayCommands: ["recipes list", "recipes show", "recipes apply", "recipes install"],
|
|
199
|
-
skills: ["oxygen-recipes", "oxygen-
|
|
199
|
+
skills: ["oxygen-recipes", "oxygen-gtm-consultant"],
|
|
200
200
|
endpointSections: ["blueprints", "recipes", "templates"],
|
|
201
201
|
intentTerms: ["recipe", "playbook", "proven play", "activation play", "setup play", "journey", "day 1", "day 7", "day 30", "blueprint", "template", "what should i do", "inbound led outbound", "engagers into a sequence", "warm outbound from my posts", "whole motion", "end to end motion", "kit"],
|
|
202
202
|
},
|
|
@@ -210,7 +210,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
210
210
|
posture: "workspace_write",
|
|
211
211
|
gatewayTools: ["oxygen_crm_record_search", "oxygen_crm_record_get"],
|
|
212
212
|
gatewayCommands: ["crm search", "crm get", "crm describe", "crm tasks list", "crm notes list"],
|
|
213
|
-
skills: ["oxygen-
|
|
213
|
+
skills: ["oxygen-crm"],
|
|
214
214
|
endpointSections: ["crm"],
|
|
215
215
|
// "reminder"/"to-do"/"open tasks" routed to workspace-access before these
|
|
216
216
|
// terms existed (blind acceptance, 2026-09-22): the customer's phrasing for
|
|
@@ -227,7 +227,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
227
227
|
posture: "mixed",
|
|
228
228
|
gatewayTools: ["oxygen_tables_create", "oxygen_tools_search", "oxygen_columns_add", "oxygen_columns_catalog", "oxygen_enrich_column_preview", "oxygen_tables_link_bulk", "oxygen_callables_manage"],
|
|
229
229
|
gatewayCommands: ["tables create", "tools search", "columns add", "columns catalog", "enrich-column preview", "tables link", "functions list", "functions draft"],
|
|
230
|
-
skills: ["oxygen-
|
|
230
|
+
skills: ["oxygen-tables", "oxygen-enrichment", "oxygen-table-tidy", "oxygen-diagnostics", "oxygen-clay-migration", "oxygen-signals"],
|
|
231
231
|
endpointSections: ["action-columns", "callables", "functions", "columns", "company-enrichment", "enrich-column", "enrichment", "projects", "supabase", "table-action-items", "table-action-runs", "table-ingestion-runs", "tables"],
|
|
232
232
|
// "link"/"join"/"connect"/"relate" route here for `tables link`. Added after a
|
|
233
233
|
// blind user eval asked for exactly "link two tables" and was routed to
|
|
@@ -272,7 +272,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
272
272
|
posture: "workspace_write",
|
|
273
273
|
gatewayTools: ["oxygen_publishing_comments", "oxygen_publishing_analytics", "oxygen_posts_get", "oxygen_posts_create"],
|
|
274
274
|
gatewayCommands: ["publishing comments list", "publishing analytics summary", "posts get", "posts create"],
|
|
275
|
-
skills: ["oxygen-
|
|
275
|
+
skills: ["oxygen-publishing"],
|
|
276
276
|
endpointSections: [],
|
|
277
277
|
intentTerms: ["post artifact", "social post", "post comments", "public comments", "unanswered comments", "comments needing reply", "owned posts", "post reactions", "post performance", "posting analytics", "broadcast content"],
|
|
278
278
|
},
|
|
@@ -286,7 +286,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
286
286
|
posture: "workspace_write",
|
|
287
287
|
gatewayTools: ["oxygen_signals_list", "oxygen_signals_leads_today", "oxygen_signals_registry", "oxygen_tags_attach"],
|
|
288
288
|
gatewayCommands: ["signals list", "signals leads-today", "signals registry", "tags attach"],
|
|
289
|
-
skills: ["oxygen-
|
|
289
|
+
skills: ["oxygen-signals"],
|
|
290
290
|
endpointSections: ["signals", "signup-leads"],
|
|
291
291
|
intentTerms: ["signal", "signals", "signal type", "tag signal", "intent", "website visit", "job change", "fundraising", "warm leads", "engager", "follower", "viewer"],
|
|
292
292
|
},
|
|
@@ -325,7 +325,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
325
325
|
posture: "paid_read",
|
|
326
326
|
gatewayTools: ["oxygen_ads_searches", "oxygen_adyntel_ads_search", "oxygen_tools_search"],
|
|
327
327
|
gatewayCommands: ["ads searches list", "ads searches save", "tools search"],
|
|
328
|
-
skills: ["oxygen-
|
|
328
|
+
skills: ["oxygen-signals"],
|
|
329
329
|
endpointSections: ["ads"],
|
|
330
330
|
intentTerms: ["ad", "ads", "advertising", "ad creative", "competitor ads", "ad intelligence", "meta ads", "linkedin ads", "saved ads search", "reusable ads search"],
|
|
331
331
|
},
|
|
@@ -380,7 +380,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
380
380
|
posture: "external_write",
|
|
381
381
|
gatewayTools: ["oxygen_publishing_posts_create", "oxygen_publishing_posts_list", "oxygen_publishing_posts_approve", "oxygen_ugc_get"],
|
|
382
382
|
gatewayCommands: ["publishing posts create", "publishing posts list", "publishing posts approve", "ugc programs list", "ugc programs duplicate", "ugc programs archive"],
|
|
383
|
-
skills: ["oxygen-
|
|
383
|
+
skills: ["oxygen-publishing", "oxygen-ugc"],
|
|
384
384
|
endpointSections: ["publishing", "ugc"],
|
|
385
385
|
// "remove program" / "delete program" landed on route: null in a blind eval
|
|
386
386
|
// (2026-09-10); a program is a UGC noun here, and its lifecycle verbs must
|
|
@@ -439,7 +439,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
439
439
|
posture: "paid_read",
|
|
440
440
|
gatewayTools: ["oxygen_companies_search_plan", "oxygen_people_search_plan", "oxygen_tools_search", "oxygen_find_company"],
|
|
441
441
|
gatewayCommands: ["companies search plan", "people search plan", "tools search", "find company"],
|
|
442
|
-
skills: ["oxygen-
|
|
442
|
+
skills: ["oxygen-sourcing", "oxygen-enrichment"],
|
|
443
443
|
endpointSections: ["companies", "find", "lead-sourcing", "people", "search", "sourcing", "tools", "verify"],
|
|
444
444
|
intentTerms: ["find companies", "find people", "source leads", "build a list", "tam", "prospect", "provider", "tool catalog", "find email", "find phone", "local businesses", "local business", "google maps", "storefronts"],
|
|
445
445
|
},
|
|
@@ -453,7 +453,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
453
453
|
posture: "paid_read",
|
|
454
454
|
gatewayTools: ["oxygen_tools_search", "oxygen_tools_get"],
|
|
455
455
|
gatewayCommands: ["tools search", "tools get"],
|
|
456
|
-
skills: ["oxygen-
|
|
456
|
+
skills: ["oxygen-signals"],
|
|
457
457
|
endpointSections: [],
|
|
458
458
|
intentTerms: ["public linkedin", "third party linkedin", "linkedin profile", "linkedin company", "linkedin post", "linkedin comments", "linkedin reactions", "linkedin engagers", "harvest", "cookieless"],
|
|
459
459
|
negativeTerms: ["my linkedin", "our linkedin", "own linkedin", "connected account", "inbox", "send", "invite", "publish"],
|
|
@@ -468,7 +468,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
468
468
|
posture: "mixed",
|
|
469
469
|
gatewayTools: ["oxygen_integrations_list", "oxygen_integrations_connect", "oxygen_integrations_list_actions"],
|
|
470
470
|
gatewayCommands: ["integrations list", "integrations connect", "custom-integrations list"],
|
|
471
|
-
skills: ["oxygen-gtm"],
|
|
471
|
+
skills: ["oxygen-tables", "oxygen-gtm"],
|
|
472
472
|
endpointSections: ["custom-integrations", "integrations"],
|
|
473
473
|
intentTerms: ["integration", "integrations", "connect provider", "oauth", "byok", "bring your own key", "provider key", "provider connection", "custom http", "custom integration", "connection readiness"],
|
|
474
474
|
},
|
|
@@ -521,7 +521,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
521
521
|
posture: "mixed",
|
|
522
522
|
gatewayTools: ["oxygen_linkedin_network_setup", "oxygen_linkedin_invitations_setup", "oxygen_senders_list", "oxygen_linkedin_connections_import", "oxygen_inbox_list"],
|
|
523
523
|
gatewayCommands: ["linkedin network setup", "linkedin invitations setup", "senders list", "connections import", "inbox list"],
|
|
524
|
-
skills: ["oxygen-
|
|
524
|
+
skills: ["oxygen-signals", "oxygen-sequencer", "oxygen-unibox"],
|
|
525
525
|
endpointSections: ["linkedin"],
|
|
526
526
|
intentTerms: ["my linkedin", "our linkedin", "own linkedin", "connected linkedin", "connections", "mutual connections", "shared connections", "connections in common", "followers", "my network", "whole network", "linkedin network", "connections and followers", "profile viewers", "sales navigator", "recruiter", "linkedin inbox", "working hours", "sending hours", "linkedin limits"],
|
|
527
527
|
},
|
|
@@ -580,8 +580,8 @@ export function inferCapabilityRoute(query) {
|
|
|
580
580
|
return {
|
|
581
581
|
card: {
|
|
582
582
|
...recipes,
|
|
583
|
-
execution: "Pick the matching audience Recipe (own, competitor, creator or keyword post engagers, company-page followers or own followers), resolve current company/ICP context, then read that Recipe and oxygen-
|
|
584
|
-
skills: ["oxygen-recipes", "oxygen-
|
|
583
|
+
execution: "Pick the matching audience Recipe (own, competitor, creator or keyword post engagers, company-page followers or own followers), resolve current company/ICP context, then read that Recipe and the oxygen-signals file references/audience-campaigns.md. Preserve the exact source, attribution, qualification rule/version and object IDs. Prepare a bounded native audience campaign for email, LinkedIn DM/connection requests or WhatsApp. Keep capture, standing qualification, router and Sequence disabled until their own approval and credit scope; the shared guide owns execution.",
|
|
584
|
+
skills: ["oxygen-recipes", "oxygen-signals"],
|
|
585
585
|
},
|
|
586
586
|
recommendedTools: ["oxygen_recipes_list", "oxygen_recipes_get", "oxygen_context_resolve"],
|
|
587
587
|
recommendedCommands: ["recipes list", "recipes show", "context resolve"],
|
|
@@ -679,28 +679,46 @@ export function inferPrimitiveRoute(query) {
|
|
|
679
679
|
return null;
|
|
680
680
|
return route;
|
|
681
681
|
}
|
|
682
|
+
/**
|
|
683
|
+
* Skills OXYGEN once served and has retired (2026-10-04: plays are inspiration
|
|
684
|
+
* read through the recipes API, never skills, and visual design is taught by
|
|
685
|
+
* its commands' own help; LinkedIn marketing split into the Publishing and
|
|
686
|
+
* Signals feature skills, with its account-safety rules in the sequencer's
|
|
687
|
+
* LinkedIn reference). The manifest no longer serves them, so nothing new
|
|
688
|
+
* can install, attach or advertise them; their audience below stays so an Agent
|
|
689
|
+
* version or Copilot session that already pinned one keeps its snapshot.
|
|
690
|
+
*/
|
|
691
|
+
export const RETIRED_PRODUCT_SKILLS = Object.freeze(["oxygen-playbooks", "oxygen-visual-design", "oxygen-linkedin-marketing"]);
|
|
682
692
|
/** Current hosted instruction audiences, shared by discovery and Agent snapshots. */
|
|
683
693
|
export const OXYGEN_PRODUCT_SKILL_AUDIENCES = Object.freeze({
|
|
684
694
|
"oxygen-onboarding": "workspace",
|
|
695
|
+
"oxygen-gtm-consultant": "workspace",
|
|
685
696
|
"oxygen-gtm": "workspace",
|
|
697
|
+
"oxygen-tables": "workspace",
|
|
698
|
+
"oxygen-enrichment": "workspace",
|
|
699
|
+
"oxygen-sourcing": "workspace",
|
|
700
|
+
"oxygen-crm": "workspace",
|
|
686
701
|
"oxygen-recipes": "workspace",
|
|
687
|
-
"oxygen-playbooks": "workspace",
|
|
688
|
-
"oxygen-linkedin-marketing": "workspace",
|
|
689
702
|
"oxygen-agency-directory": "agency_partner",
|
|
690
703
|
"oxygen-collaboration": "workspace",
|
|
691
704
|
"oxygen-quickstart": "workspace",
|
|
692
705
|
"oxygen-workflow-authoring": "workspace",
|
|
693
706
|
"oxygen-knowledge": "workspace",
|
|
694
|
-
"oxygen-visual-design": "workspace",
|
|
695
707
|
"oxygen-unibox": "workspace",
|
|
696
708
|
"oxygen-email-infra": "workspace",
|
|
697
709
|
"oxygen-deliverability": "workspace",
|
|
698
710
|
"oxygen-sequencer": "workspace",
|
|
711
|
+
"oxygen-publishing": "workspace",
|
|
712
|
+
"oxygen-signals": "workspace",
|
|
699
713
|
"oxygen-table-tidy": "workspace",
|
|
700
714
|
"oxygen-diagnostics": "workspace",
|
|
701
715
|
"oxygen-feedback": "workspace",
|
|
702
716
|
"oxygen-clay-migration": "workspace",
|
|
703
717
|
"oxygen-ugc": "workspace",
|
|
718
|
+
// Retired (RETIRED_PRODUCT_SKILLS): readable for existing pins, never served.
|
|
719
|
+
"oxygen-playbooks": "workspace",
|
|
720
|
+
"oxygen-visual-design": "workspace",
|
|
721
|
+
"oxygen-linkedin-marketing": "workspace",
|
|
704
722
|
});
|
|
705
723
|
export function canReadProductSkillInstructions(name, entitlements) {
|
|
706
724
|
if (entitlements.authenticated !== true)
|
|
@@ -747,9 +765,11 @@ export function readProductSkillInstructionResult(capabilityName, result) {
|
|
|
747
765
|
}
|
|
748
766
|
return null;
|
|
749
767
|
}
|
|
750
|
-
/** Public action cards never advertise invite-only or unknown skill instructions. */
|
|
768
|
+
/** Public action cards never advertise invite-only, retired or unknown skill instructions. */
|
|
751
769
|
export function isPublicCapabilitySkill(name) {
|
|
752
|
-
return Object.hasOwn(OXYGEN_PRODUCT_SKILL_AUDIENCES, name)
|
|
770
|
+
return Object.hasOwn(OXYGEN_PRODUCT_SKILL_AUDIENCES, name)
|
|
771
|
+
&& OXYGEN_PRODUCT_SKILL_AUDIENCES[name] === "workspace"
|
|
772
|
+
&& !RETIRED_PRODUCT_SKILLS.includes(name);
|
|
753
773
|
}
|
|
754
774
|
export function serializeCapabilityRoute(route, surfaceOrOptions = "cli", options = {}) {
|
|
755
775
|
const surface = typeof surfaceOrOptions === "string" ? surfaceOrOptions : "cli";
|
|
@@ -930,7 +950,7 @@ const UGC_ROUTE_VIEW = {
|
|
|
930
950
|
notFor: "Scheduling the workspace's own posts, one-to-one messaging, outreach campaigns, or keyword post search.",
|
|
931
951
|
execution: "Brands create a program from its product URL and invite creators; creators apply or accept in their own workspace. A post publishes only after the creator approves its exact content, or the program organizer does when the creator delegated approval; brands cannot require approval. Paid drafts, voice imports and amplification preview first and need an approved credit ceiling.",
|
|
932
952
|
posture: "mixed",
|
|
933
|
-
skills: ["oxygen-ugc", "oxygen-
|
|
953
|
+
skills: ["oxygen-ugc", "oxygen-publishing"],
|
|
934
954
|
};
|
|
935
955
|
/** The card a route reports: Publishing's own, or its UGC view for a creator-program ask. */
|
|
936
956
|
function routeCardFor(card, query) {
|
|
@@ -996,14 +1016,18 @@ function preferredSkillForIntent(card, query) {
|
|
|
996
1016
|
if (/\b(stuck|failed|failure|trace|provenance|diagnos\w*)\b/.test(query))
|
|
997
1017
|
return "oxygen-diagnostics";
|
|
998
1018
|
if (isLinkedInProfileWatcherIntent(query))
|
|
999
|
-
return "oxygen-
|
|
1019
|
+
return "oxygen-signals";
|
|
1000
1020
|
}
|
|
1021
|
+
// One person's email or phone is a one-shot lookup the enrichment skill
|
|
1022
|
+
// teaches (`find email`, `find phone`); list building stays with sourcing.
|
|
1023
|
+
if (card.id === "sourcing-and-provider-tools" && oneShotContactFindingRecommendation(query))
|
|
1024
|
+
return "oxygen-enrichment";
|
|
1001
1025
|
if (card.id === "billing-and-budgets" && /\b(explain|why|reduce|increas\w*|burn|spent)\b/.test(query))
|
|
1002
1026
|
return "oxygen-diagnostics";
|
|
1003
1027
|
if (card.id === "sending-infrastructure" && /\b(bounc\w*|health|placement|deliverability|spam)\b/.test(query))
|
|
1004
1028
|
return "oxygen-deliverability";
|
|
1005
1029
|
if (["signals", "linkedin-public-research"].includes(card.id) && /\blinkedin\b/.test(query))
|
|
1006
|
-
return "oxygen-
|
|
1030
|
+
return "oxygen-signals";
|
|
1007
1031
|
return null;
|
|
1008
1032
|
}
|
|
1009
1033
|
function isTableMaintenanceIntent(query) {
|
|
@@ -76,7 +76,7 @@ export const SENDER_LIMITS_SET_OPERATION = {
|
|
|
76
76
|
contract: SENDER_LIMITS_PATCH_CONTRACT,
|
|
77
77
|
cli: {
|
|
78
78
|
path: ["senders", "limits", "set"],
|
|
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-
|
|
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-sequencer --file references/linkedin.md` first. Changing limits is free. <id> accepts a sender account id, connection id, or Unipile account id.",
|
|
80
80
|
argument: { name: "id", description: "Sender account id, connection id, or Unipile account id." },
|
|
81
81
|
},
|
|
82
82
|
mcp: {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// GENERATED by apps/web/scripts/generate-copilot-journeys.ts from the recipe
|
|
2
|
-
// catalog (`journey:` blocks in apps/web/src/
|
|
2
|
+
// catalog (`journey:` blocks in apps/web/src/lib/recipes/plays).
|
|
3
3
|
// Do not edit by hand — run `npm run skills:metadata -w @oxygen/web`.
|
|
4
4
|
export const COPILOT_JOURNEYS_GENERATED = [
|
|
5
5
|
{
|
|
@@ -1,25 +1,19 @@
|
|
|
1
1
|
import { COPILOT_PLAYBOOKS_GENERATED } from "./copilot-playbooks.generated.js";
|
|
2
2
|
export type CopilotPlaybookSlug = (typeof COPILOT_PLAYBOOKS_GENERATED)[number]["slug"];
|
|
3
3
|
export type CopilotPlaybook = {
|
|
4
|
-
/**
|
|
5
|
-
* The skill slug the Copilot loads through its `skills` tool, and the
|
|
6
|
-
* `name:` in the playbook file's frontmatter. The two are gated equal.
|
|
7
|
-
*/
|
|
4
|
+
/** The featured play's catalog slug: the play the Copilot reads before it works. */
|
|
8
5
|
slug: CopilotPlaybookSlug;
|
|
9
6
|
/** Customer-facing name, exactly as it is spoken in the composer prompt and shown on the Home chip. */
|
|
10
7
|
title: string;
|
|
11
|
-
/** The featured catalog recipe that carries this play: its kit, prerequisites and approval gates. */
|
|
12
|
-
recipeSlug: string;
|
|
13
8
|
/** The play's request in the person's own words, the same text its card copies (2026-10-03). */
|
|
14
9
|
prompt: string;
|
|
15
10
|
};
|
|
16
11
|
export declare const COPILOT_PLAYBOOKS: readonly CopilotPlaybook[];
|
|
17
12
|
/**
|
|
18
13
|
* What a chip writes into the composer: the play's own request, the same words
|
|
19
|
-
* its prompt card copies for the person's agent (plays as prompts,
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
14
|
+
* its prompt card copies for the person's agent (plays as prompts, 2026-10-03),
|
|
15
|
+
* then the play named the way a person says it. The Copilot's prompt maps that
|
|
16
|
+
* title back to the catalog slug, so the user reads and sends exactly what the
|
|
17
|
+
* chip promised.
|
|
23
18
|
*/
|
|
24
19
|
export declare function copilotPlaybookPrompt(playbook: Pick<CopilotPlaybook, "title" | "prompt">): string;
|
|
25
|
-
export declare function getCopilotPlaybook(slug: string): CopilotPlaybook | null;
|
|
@@ -1,21 +1,17 @@
|
|
|
1
1
|
export declare const COPILOT_PLAYBOOKS_GENERATED: readonly [{
|
|
2
|
-
readonly slug: "
|
|
2
|
+
readonly slug: "icp-to-account-map";
|
|
3
3
|
readonly title: "TAM sourcing";
|
|
4
|
-
readonly recipeSlug: "icp-to-account-map";
|
|
5
4
|
readonly prompt: "Turn our ICP into a scored list of target accounts, with the evidence behind each score, so I can see which segments fit best.";
|
|
6
5
|
}, {
|
|
7
|
-
readonly slug: "
|
|
6
|
+
readonly slug: "founder-posting-system";
|
|
8
7
|
readonly title: "LinkedIn content strategy";
|
|
9
|
-
readonly recipeSlug: "founder-posting-system";
|
|
10
8
|
readonly prompt: "Help me post on LinkedIn consistently in my own voice: draft posts from our positioning and ICP, let me approve each one, and track how they perform.";
|
|
11
9
|
}, {
|
|
12
10
|
readonly slug: "inbound-led-outbound";
|
|
13
11
|
readonly title: "Inbound-led outbound";
|
|
14
|
-
readonly recipeSlug: "inbound-led-outbound";
|
|
15
12
|
readonly prompt: "Turn the people who react to and comment on LinkedIn posts I pick into pipeline: score them against our ICP and message only the good fits from a capped sequence.";
|
|
16
13
|
}, {
|
|
17
|
-
readonly slug: "
|
|
14
|
+
readonly slug: "daily-warm-lead-queue";
|
|
18
15
|
readonly title: "Signal-based outbound";
|
|
19
|
-
readonly recipeSlug: "daily-warm-lead-queue";
|
|
20
16
|
readonly prompt: "Give me one ranked list every morning of the warmest people to contact, from website visitors, profile viewers, post engagers and new followers.";
|
|
21
17
|
}];
|
|
@@ -1,30 +1,26 @@
|
|
|
1
|
-
// GENERATED by apps/web/scripts/generate-copilot-playbooks.ts from the
|
|
2
|
-
// catalog (featured
|
|
3
|
-
// apps/web/src/
|
|
1
|
+
// GENERATED by apps/web/scripts/generate-copilot-playbooks.ts from the play
|
|
2
|
+
// catalog (featured plays: `featured_order`, `label`, `prompt` in
|
|
3
|
+
// apps/web/src/lib/recipes/plays).
|
|
4
4
|
// Do not edit by hand — run `npm run skills:metadata -w @oxygen/web`.
|
|
5
5
|
export const COPILOT_PLAYBOOKS_GENERATED = [
|
|
6
6
|
{
|
|
7
|
-
slug: "
|
|
7
|
+
slug: "icp-to-account-map",
|
|
8
8
|
title: "TAM sourcing",
|
|
9
|
-
recipeSlug: "icp-to-account-map",
|
|
10
9
|
prompt: "Turn our ICP into a scored list of target accounts, with the evidence behind each score, so I can see which segments fit best.",
|
|
11
10
|
},
|
|
12
11
|
{
|
|
13
|
-
slug: "
|
|
12
|
+
slug: "founder-posting-system",
|
|
14
13
|
title: "LinkedIn content strategy",
|
|
15
|
-
recipeSlug: "founder-posting-system",
|
|
16
14
|
prompt: "Help me post on LinkedIn consistently in my own voice: draft posts from our positioning and ICP, let me approve each one, and track how they perform.",
|
|
17
15
|
},
|
|
18
16
|
{
|
|
19
17
|
slug: "inbound-led-outbound",
|
|
20
18
|
title: "Inbound-led outbound",
|
|
21
|
-
recipeSlug: "inbound-led-outbound",
|
|
22
19
|
prompt: "Turn the people who react to and comment on LinkedIn posts I pick into pipeline: score them against our ICP and message only the good fits from a capped sequence.",
|
|
23
20
|
},
|
|
24
21
|
{
|
|
25
|
-
slug: "
|
|
22
|
+
slug: "daily-warm-lead-queue",
|
|
26
23
|
title: "Signal-based outbound",
|
|
27
|
-
recipeSlug: "daily-warm-lead-queue",
|
|
28
24
|
prompt: "Give me one ranked list every morning of the warmest people to contact, from website visitors, profile viewers, post engagers and new followers.",
|
|
29
25
|
},
|
|
30
26
|
];
|
|
@@ -2,14 +2,11 @@ import { COPILOT_PLAYBOOKS_GENERATED } from "./copilot-playbooks.generated.js";
|
|
|
2
2
|
export const COPILOT_PLAYBOOKS = COPILOT_PLAYBOOKS_GENERATED;
|
|
3
3
|
/**
|
|
4
4
|
* What a chip writes into the composer: the play's own request, the same words
|
|
5
|
-
* its prompt card copies for the person's agent (plays as prompts,
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
5
|
+
* its prompt card copies for the person's agent (plays as prompts, 2026-10-03),
|
|
6
|
+
* then the play named the way a person says it. The Copilot's prompt maps that
|
|
7
|
+
* title back to the catalog slug, so the user reads and sends exactly what the
|
|
8
|
+
* chip promised.
|
|
9
9
|
*/
|
|
10
10
|
export function copilotPlaybookPrompt(playbook) {
|
|
11
|
-
return `${playbook.prompt} Use the ${playbook.title}
|
|
12
|
-
}
|
|
13
|
-
export function getCopilotPlaybook(slug) {
|
|
14
|
-
return COPILOT_PLAYBOOKS.find((playbook) => playbook.slug === slug) ?? null;
|
|
11
|
+
return `${playbook.prompt} Use the ${playbook.title} play.`;
|
|
15
12
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const OXYGEN_BUILD_VERSION = "1.1011.
|
|
1
|
+
export declare const OXYGEN_BUILD_VERSION = "1.1011.677";
|
|
@@ -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.
|
|
2
|
+
export const OXYGEN_BUILD_VERSION = "1.1011.677";
|