@oxygen-agent/cli 1.1014.136 → 1.1014.619
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/admin-primary-providers-render.js +27 -19
- package/dist/cli-output.d.ts +12 -0
- package/dist/cli-output.js +395 -0
- package/dist/cli-paths.d.ts +1 -0
- package/dist/cli-paths.js +3 -0
- package/dist/cli-profile-context.d.ts +3 -0
- package/dist/cli-profile-context.js +13 -0
- package/dist/command-manifest.d.ts +15 -0
- package/dist/command-manifest.js +23 -1
- package/dist/command-support.d.ts +438 -0
- package/dist/command-support.js +1610 -0
- package/dist/commands/action-column.d.ts +3 -0
- package/dist/commands/action-column.js +43 -0
- package/dist/commands/activation.d.ts +3 -0
- package/dist/commands/activation.js +24 -0
- package/dist/commands/admin.d.ts +3 -0
- package/dist/commands/admin.js +347 -0
- package/dist/commands/ads.d.ts +3 -0
- package/dist/commands/ads.js +62 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +274 -0
- package/dist/commands/api-keys.d.ts +3 -0
- package/dist/commands/api-keys.js +77 -0
- package/dist/commands/approvals.d.ts +3 -0
- package/dist/commands/approvals.js +518 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +230 -0
- package/dist/commands/billing.d.ts +3 -0
- package/dist/commands/billing.js +357 -0
- package/dist/commands/blueprints.d.ts +3 -0
- package/dist/commands/blueprints.js +312 -0
- package/dist/commands/budget.d.ts +3 -0
- package/dist/commands/budget.js +55 -0
- package/dist/commands/callables.d.ts +3 -0
- package/dist/commands/callables.js +111 -0
- package/dist/commands/capabilities.d.ts +3 -0
- package/dist/commands/capabilities.js +44 -0
- package/dist/commands/cells.d.ts +3 -0
- package/dist/commands/cells.js +94 -0
- package/dist/commands/columns.d.ts +3 -0
- package/dist/commands/columns.js +1628 -0
- package/dist/commands/commands.d.ts +3 -0
- package/dist/commands/commands.js +45 -0
- package/dist/commands/comments.d.ts +3 -0
- package/dist/commands/comments.js +188 -0
- package/dist/commands/companies.d.ts +3 -0
- package/dist/commands/companies.js +312 -0
- package/dist/commands/connections.d.ts +3 -0
- package/dist/commands/connections.js +39 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +170 -0
- package/dist/commands/copilot.d.ts +3 -0
- package/dist/commands/copilot.js +565 -0
- package/dist/commands/crm.d.ts +3 -0
- package/dist/commands/crm.js +1672 -0
- package/dist/commands/custom-integrations.d.ts +3 -0
- package/dist/commands/custom-integrations.js +140 -0
- package/dist/commands/dashboards.d.ts +3 -0
- package/dist/commands/dashboards.js +240 -0
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.js +190 -0
- package/dist/commands/deliverability.d.ts +3 -0
- package/dist/commands/deliverability.js +90 -0
- package/dist/commands/directory.d.ts +3 -0
- package/dist/commands/directory.js +101 -0
- package/dist/commands/domains.d.ts +3 -0
- package/dist/commands/domains.js +604 -0
- package/dist/commands/egress.d.ts +3 -0
- package/dist/commands/egress.js +130 -0
- package/dist/commands/email.d.ts +3 -0
- package/dist/commands/email.js +49 -0
- package/dist/commands/engagement.d.ts +3 -0
- package/dist/commands/engagement.js +189 -0
- package/dist/commands/enrich-column.d.ts +3 -0
- package/dist/commands/enrich-column.js +149 -0
- package/dist/commands/enrichment.d.ts +3 -0
- package/dist/commands/enrichment.js +49 -0
- package/dist/commands/feedback.d.ts +3 -0
- package/dist/commands/feedback.js +96 -0
- package/dist/commands/feeds.d.ts +3 -0
- package/dist/commands/feeds.js +486 -0
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.js +159 -0
- package/dist/commands/followers.d.ts +3 -0
- package/dist/commands/followers.js +39 -0
- package/dist/commands/formulas.d.ts +3 -0
- package/dist/commands/formulas.js +43 -0
- package/dist/commands/home.d.ts +3 -0
- package/dist/commands/home.js +22 -0
- package/dist/commands/inbox.d.ts +3 -0
- package/dist/commands/inbox.js +779 -0
- package/dist/commands/integrations.d.ts +3 -0
- package/dist/commands/integrations.js +290 -0
- package/dist/commands/knowledge.d.ts +3 -0
- package/dist/commands/knowledge.js +874 -0
- package/dist/commands/lead-sourcing.d.ts +3 -0
- package/dist/commands/lead-sourcing.js +52 -0
- package/dist/commands/limits.d.ts +3 -0
- package/dist/commands/limits.js +54 -0
- package/dist/commands/linkedin.d.ts +3 -0
- package/dist/commands/linkedin.js +275 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +347 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/mailboxes.d.ts +3 -0
- package/dist/commands/mailboxes.js +1290 -0
- package/dist/commands/managed-inboxes.d.ts +3 -0
- package/dist/commands/managed-inboxes.js +361 -0
- package/dist/commands/messages.d.ts +3 -0
- package/dist/commands/messages.js +78 -0
- package/dist/commands/notetaker.d.ts +3 -0
- package/dist/commands/notetaker.js +79 -0
- package/dist/commands/observability.d.ts +3 -0
- package/dist/commands/observability.js +125 -0
- package/dist/commands/orgs.d.ts +3 -0
- package/dist/commands/orgs.js +237 -0
- package/dist/commands/people.d.ts +3 -0
- package/dist/commands/people.js +353 -0
- package/dist/commands/posts.d.ts +3 -0
- package/dist/commands/posts.js +139 -0
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +250 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +94 -0
- package/dist/commands/prompts.d.ts +3 -0
- package/dist/commands/prompts.js +84 -0
- package/dist/commands/publishing.d.ts +3 -0
- package/dist/commands/publishing.js +1419 -0
- package/dist/commands/recipes.d.ts +3 -0
- package/dist/commands/recipes.js +120 -0
- package/dist/commands/reviews.d.ts +3 -0
- package/dist/commands/reviews.js +75 -0
- package/dist/commands/rows.d.ts +3 -0
- package/dist/commands/rows.js +27 -0
- package/dist/commands/runs.d.ts +3 -0
- package/dist/commands/runs.js +37 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.js +182 -0
- package/dist/commands/senders.d.ts +3 -0
- package/dist/commands/senders.js +393 -0
- package/dist/commands/sequences.d.ts +3 -0
- package/dist/commands/sequences.js +2322 -0
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.js +109 -0
- package/dist/commands/signals.d.ts +3 -0
- package/dist/commands/signals.js +289 -0
- package/dist/commands/signup-leads.d.ts +3 -0
- package/dist/commands/signup-leads.js +46 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.js +50 -0
- package/dist/commands/sourcing.d.ts +3 -0
- package/dist/commands/sourcing.js +27 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +51 -0
- package/dist/commands/supabase.d.ts +3 -0
- package/dist/commands/supabase.js +128 -0
- package/dist/commands/support.d.ts +3 -0
- package/dist/commands/support.js +651 -0
- package/dist/commands/suppressions.d.ts +3 -0
- package/dist/commands/suppressions.js +577 -0
- package/dist/commands/table-ingestions.d.ts +3 -0
- package/dist/commands/table-ingestions.js +145 -0
- package/dist/commands/table-runs.d.ts +3 -0
- package/dist/commands/table-runs.js +277 -0
- package/dist/commands/tables.d.ts +3 -0
- package/dist/commands/tables.js +3505 -0
- package/dist/commands/tags.d.ts +3 -0
- package/dist/commands/tags.js +147 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.js +246 -0
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +106 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.js +27 -0
- package/dist/commands/viewers.d.ts +3 -0
- package/dist/commands/viewers.js +72 -0
- package/dist/commands/voice.d.ts +3 -0
- package/dist/commands/voice.js +351 -0
- package/dist/commands/whatsapp.d.ts +3 -0
- package/dist/commands/whatsapp.js +226 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.js +110 -0
- package/dist/commands/worker.d.ts +3 -0
- package/dist/commands/worker.js +60 -0
- package/dist/commands/workflows.d.ts +3 -0
- package/dist/commands/workflows.js +1752 -0
- package/dist/commands/workspace.d.ts +3 -0
- package/dist/commands/workspace.js +94 -0
- package/dist/compact-list-flags.d.ts +7 -0
- package/dist/compact-list-flags.js +12 -0
- package/dist/crm-support-flags.js +14 -5
- package/dist/customer-operation-commands.js +8 -6
- package/dist/diagnostic-uploader.d.ts +3 -1
- package/dist/diagnostic-uploader.js +29 -8
- package/dist/help.d.ts +5 -0
- package/dist/help.js +22 -0
- package/dist/index.d.ts +98 -1
- package/dist/index.js +331 -30719
- package/dist/local-module-loader.d.ts +4 -0
- package/dist/local-module-loader.js +156 -0
- package/dist/visual-commands.js +9 -5
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.js +69 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +14 -5
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +14 -3
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +175 -14
- package/node_modules/@oxygen/shared/dist/compact-list-result.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/compact-list-result.js +27 -0
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +12 -1
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +5 -2
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.js +23 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -2
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +20 -16
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.d.ts +49 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.js +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +60 -7
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +13 -5
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/teammates.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +115 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +21 -3
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +31 -1
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +56 -15
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +71 -44
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +348 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.js +42 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +59 -213
- package/node_modules/@oxygen/shared/dist/diagnostics.js +3 -2
- package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +16 -5
- package/node_modules/@oxygen/shared/dist/email-dsn.js +97 -1
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +5 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +4 -5
- package/node_modules/@oxygen/shared/dist/file-import.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +21 -2
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +20 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -2
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +17 -7
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +81 -17
- package/node_modules/@oxygen/shared/dist/langfuse.js +8 -4
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +6 -3
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +9 -9
- package/node_modules/@oxygen/shared/dist/mcp-server-info.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/mcp-server-info.js +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +14 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +17 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
- package/node_modules/@oxygen/shared/dist/provider-selection.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/provider-selection.js +3 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/repricing.js +2 -2
- package/node_modules/@oxygen/shared/dist/schedule-label.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/schedule-label.js +12 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +20 -6
- package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +55 -0
- package/node_modules/@oxygen/shared/dist/teammate-groups.js +74 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.js +88 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.js +73 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.d.ts +95 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.js +266 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.d.ts +56 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.js +114 -0
- package/node_modules/@oxygen/shared/dist/teammates.d.ts +78 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +140 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +120 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/visual-render.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/visual-render.js +12 -5
- package/node_modules/@oxygen/shared/dist/warmup-metrics.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/warmup-metrics.js +44 -0
- package/package.json +1 -1
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
import { parseJsonObject, readNonNegativeInt, readPositiveInt } from "../cli-values.js";
|
|
2
|
+
import { readCreditCeilingOrZero, readCsvOption, readFileIfPresent, readRangeOption, readSearchPlanJson } from "../command-support.js";
|
|
3
|
+
import { requestOxygen } from "../http-client.js";
|
|
4
|
+
import { readOption } from "../util.js";
|
|
5
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
// Table-driven Employee Finder help, written once and shared by `people search
|
|
8
|
+
// plan` and `people search run` so the price shape reads the same on both.
|
|
9
|
+
const PEOPLE_SEARCH_FROM_TABLE_HELP = "Search every company LinkedIn URL in a table column for the people you describe — --titles, --exclude-titles, --seniorities, --job-functions, --education, location and --min-connections all apply per company. 1 credit per company per page; rows land in a new people table with a company relation back to each source row. Pass the table id or slug plus --linkedin-url-column, and bound it with --company-limit. For up to 50 companies this is the expensive way: people search preview --from-table samples them free and people search run --source people_search charges one page across all of them.";
|
|
10
|
+
const PEOPLE_SEARCH_LINKEDIN_COLUMN_HELP = "Column key in --from-table holding each company's LinkedIn URL.";
|
|
11
|
+
const PEOPLE_SEARCH_COMPANY_OFFSET_HELP = "Skip this many companies in --from-table. Use it to source the rest after a first-company preview.";
|
|
12
|
+
const PEOPLE_SEARCH_COMPANY_LIMIT_HELP = "Fetch people for at most this many companies from --from-table. Start with --company-limit 1.";
|
|
13
|
+
const PEOPLE_SEARCH_JOB_FUNCTIONS_HELP = "Comma-separated job functions: Sales, Marketing, Engineering, Finance, ...";
|
|
14
|
+
const PEOPLE_SEARCH_CONTINENTS_HELP = "Comma-separated continents: Europe, North America, Asia, ...";
|
|
15
|
+
const PEOPLE_SEARCH_SALES_REGIONS_HELP = "Comma-separated sales regions: EMEA, NAMER, APAC, LATAM.";
|
|
16
|
+
const PEOPLE_SEARCH_MIN_CONNECTIONS_HELP = "Only return people with at least this many LinkedIn connections (0-500).";
|
|
17
|
+
function readPeopleSearchSource(options) {
|
|
18
|
+
const table = readOption(options.fromTable);
|
|
19
|
+
const column = readOption(options.linkedinUrlColumn);
|
|
20
|
+
if (!table && !column)
|
|
21
|
+
return null;
|
|
22
|
+
if (!table || !column) {
|
|
23
|
+
throw new OxygenError("invalid_request", "--from-table and --linkedin-url-column go together: name the table holding your companies and the column holding each company's LinkedIn URL.", { details: { from_table: table ?? null, linkedin_url_column: column ?? null }, exitCode: 1 });
|
|
24
|
+
}
|
|
25
|
+
return { table, linkedin_url_column: column, ...readPeopleSearchCompanyWindow(options) };
|
|
26
|
+
}
|
|
27
|
+
function readPeopleSearchCompanyWindow(options) {
|
|
28
|
+
const offset = readNonNegativeInt(options.companyOffset);
|
|
29
|
+
const limit = readPositiveInt(options.companyLimit);
|
|
30
|
+
return {
|
|
31
|
+
...(offset !== undefined ? { company_offset: offset } : {}),
|
|
32
|
+
...(limit !== undefined ? { company_limit: limit } : {}),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
// `--pages-per-company` is the customer-readable spelling of `--max-pages` on a
|
|
36
|
+
// table-driven run, where a page is one provider request per company. Two spellings
|
|
37
|
+
// of one ceiling must never disagree silently on a paid run.
|
|
38
|
+
function readPeopleSearchMaxPages(options) {
|
|
39
|
+
const perCompany = readPositiveInt(options.pagesPerCompany);
|
|
40
|
+
const maxPages = readPositiveInt(options.maxPages);
|
|
41
|
+
if (perCompany !== undefined && maxPages !== undefined && perCompany !== maxPages) {
|
|
42
|
+
throw new OxygenError("invalid_request", "--pages-per-company and --max-pages set the same ceiling. Pass one of them.", { details: { pages_per_company: perCompany, max_pages: maxPages }, exitCode: 1 });
|
|
43
|
+
}
|
|
44
|
+
return perCompany ?? maxPages;
|
|
45
|
+
}
|
|
46
|
+
// A customer pointing at a table has already said what they want; making them
|
|
47
|
+
// also invent a sentence is a question with one answer. The synthesized prompt is
|
|
48
|
+
// the same one the web wizard sends, so both surfaces plan from identical input.
|
|
49
|
+
function readPeopleSearchPrompt(options, source) {
|
|
50
|
+
if (options.prompt)
|
|
51
|
+
return readFileIfPresent(options.prompt);
|
|
52
|
+
if (!source)
|
|
53
|
+
return null;
|
|
54
|
+
return `Employees at companies in ${String(source.table)}`;
|
|
55
|
+
}
|
|
56
|
+
function readPeopleSearchPlanBody(options) {
|
|
57
|
+
const targetCount = readPositiveInt(options.targetCount);
|
|
58
|
+
const filters = readPeopleSearchFilters(options);
|
|
59
|
+
const source = readPeopleSearchSource(options);
|
|
60
|
+
const prompt = readPeopleSearchPrompt(options, source);
|
|
61
|
+
if (!prompt) {
|
|
62
|
+
throw new OxygenError("invalid_request", "Pass --prompt, or --from-table with --linkedin-url-column to source employees from a table you already have.", { exitCode: 1 });
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
prompt,
|
|
66
|
+
...(targetCount !== undefined ? { target_count: targetCount } : {}),
|
|
67
|
+
...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
|
|
68
|
+
...(source ? { source } : {}),
|
|
69
|
+
...(filters ? { filters } : {}),
|
|
70
|
+
// Free sizing is on by default; only --no-estimate (options.estimate === false) opts out.
|
|
71
|
+
estimate: options.estimate !== false,
|
|
72
|
+
...(options.materializePreview ? { materialize_preview: true } : {}),
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
function readPeopleSearchRunBody(options) {
|
|
76
|
+
const plan = options.planJson ? readSearchPlanJson(options.planJson) : null;
|
|
77
|
+
// --source people_search is the Find people lane: the free preview's filters
|
|
78
|
+
// ARE the request, so no prompt, plan or companies table is synthesized.
|
|
79
|
+
if (options.source !== undefined && options.source !== "people_search") {
|
|
80
|
+
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 });
|
|
81
|
+
}
|
|
82
|
+
if (options.source === "people_search") {
|
|
83
|
+
if (!options.sourceFiltersJson) {
|
|
84
|
+
throw new OxygenError("invalid_request", "--source people_search needs --source-filters-json (the same filters as people search preview).", { exitCode: 1 });
|
|
85
|
+
}
|
|
86
|
+
const maxCreditsNative = readCreditCeilingOrZero(options.maxCredits);
|
|
87
|
+
const nativeTarget = readPositiveInt(options.targetCount);
|
|
88
|
+
// Forwarded so the server refuses them by name; the native lane reads only
|
|
89
|
+
// --source-filters-json (OXP-1.39.2 S6).
|
|
90
|
+
const plannerFilters = readPeopleSearchFilters(options);
|
|
91
|
+
return {
|
|
92
|
+
source: "people_search",
|
|
93
|
+
source_filters: parseJsonObject(readFileIfPresent(options.sourceFiltersJson)),
|
|
94
|
+
...(plannerFilters ? { filters: plannerFilters } : {}),
|
|
95
|
+
...(readOption(options.tableName) ? { table_name: readOption(options.tableName) } : {}),
|
|
96
|
+
...(options.table ? { table: options.table } : {}),
|
|
97
|
+
...(options.project ? { project: options.project } : {}),
|
|
98
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
99
|
+
...(maxCreditsNative !== undefined ? { max_credits: maxCreditsNative } : {}),
|
|
100
|
+
...(nativeTarget !== undefined ? { target_count: nativeTarget } : {}),
|
|
101
|
+
...(options.approved ? { approved: true } : {}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
const source = readPeopleSearchSource(options);
|
|
105
|
+
// --from-table is itself the request, so it satisfies the prompt requirement the
|
|
106
|
+
// same way --plan-json does. A submitted plan already carries its own source and
|
|
107
|
+
// prompt, so nothing is synthesized on top of one.
|
|
108
|
+
const prompt = readPeopleSearchPrompt(options, plan ? null : source);
|
|
109
|
+
if (!prompt && !plan) {
|
|
110
|
+
throw new OxygenError("invalid_request", "Pass --prompt, --plan-json, --from-table with --linkedin-url-column, or --source people_search with --source-filters-json.", { exitCode: 1 });
|
|
111
|
+
}
|
|
112
|
+
const maxPages = readPeopleSearchMaxPages(options);
|
|
113
|
+
const maxCredits = readCreditCeilingOrZero(options.maxCredits);
|
|
114
|
+
const targetCount = readPositiveInt(options.targetCount);
|
|
115
|
+
const filters = prompt ? readPeopleSearchFilters(options) : null;
|
|
116
|
+
const sourceOverrides = source ? {} : readPeopleSearchCompanyWindow(options);
|
|
117
|
+
return {
|
|
118
|
+
...(prompt ? { prompt } : {}),
|
|
119
|
+
...(plan ? { plan } : {}),
|
|
120
|
+
...(options.routeId ? { route_id: options.routeId } : {}),
|
|
121
|
+
...(options.toolId ? { tool_id: options.toolId } : {}),
|
|
122
|
+
...(options.table ? { table: options.table } : {}),
|
|
123
|
+
...(options.project ? { project: options.project } : {}),
|
|
124
|
+
...(options.upsertKey ? { upsert_key: options.upsertKey } : {}),
|
|
125
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
126
|
+
...(maxPages !== undefined ? { max_pages: maxPages } : {}),
|
|
127
|
+
...(maxCredits !== undefined ? { max_credits: maxCredits } : {}),
|
|
128
|
+
...(targetCount !== undefined ? { target_count: targetCount } : {}),
|
|
129
|
+
...(options.sourceIntent ? { source_intent: options.sourceIntent } : {}),
|
|
130
|
+
...(source ? { source } : {}),
|
|
131
|
+
...(Object.keys(sourceOverrides).length > 0 ? { source_overrides: sourceOverrides } : {}),
|
|
132
|
+
...(filters ? { filters } : {}),
|
|
133
|
+
// Free sizing is on by default when planning from a prompt; --no-estimate opts out.
|
|
134
|
+
...(prompt ? { estimate: options.estimate !== false } : {}),
|
|
135
|
+
...(options.approved ? { approved: true } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
// PeopleSearchFilters mirror (structural — the server validates the shape). Builds a
|
|
139
|
+
// filters object from individual --titles/--seniorities/etc. flags, then lets --filters-json
|
|
140
|
+
// win per top-level filter path so an agent can pass a precise object while still using
|
|
141
|
+
// convenience flags for the rest.
|
|
142
|
+
function readPeopleSearchFilters(options) {
|
|
143
|
+
const filters = {};
|
|
144
|
+
const titles = {};
|
|
145
|
+
const titleInclude = readCsvOption(options.titles);
|
|
146
|
+
if (titleInclude.length > 0)
|
|
147
|
+
titles.include = titleInclude;
|
|
148
|
+
const adjacent = readCsvOption(options.adjacentTitles);
|
|
149
|
+
if (adjacent.length > 0)
|
|
150
|
+
titles.include_adjacent = adjacent;
|
|
151
|
+
const titleExclude = readCsvOption(options.excludeTitles);
|
|
152
|
+
if (titleExclude.length > 0)
|
|
153
|
+
titles.exclude = titleExclude;
|
|
154
|
+
const titleMatch = options.titleMatch?.trim().toLowerCase();
|
|
155
|
+
if (titleMatch) {
|
|
156
|
+
if (titleMatch !== "keyword" && titleMatch !== "exact") {
|
|
157
|
+
throw new OxygenError("invalid_title_match", '--title-match must be "keyword" or "exact".', {
|
|
158
|
+
details: { flag: "--title-match", value: options.titleMatch },
|
|
159
|
+
exitCode: 1,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
titles.match_mode = titleMatch;
|
|
163
|
+
}
|
|
164
|
+
if (Object.keys(titles).length > 0)
|
|
165
|
+
filters.titles = titles;
|
|
166
|
+
const seniorities = readCsvOption(options.seniorities);
|
|
167
|
+
if (seniorities.length > 0)
|
|
168
|
+
filters.seniorities = { include: seniorities };
|
|
169
|
+
const departments = readCsvOption(options.departments);
|
|
170
|
+
if (departments.length > 0)
|
|
171
|
+
filters.departments = { include: departments };
|
|
172
|
+
const jobFunctions = readCsvOption(options.jobFunctions);
|
|
173
|
+
if (jobFunctions.length > 0)
|
|
174
|
+
filters.job_functions = jobFunctions;
|
|
175
|
+
const keywords = readCsvOption(options.keywords);
|
|
176
|
+
if (keywords.length > 0)
|
|
177
|
+
filters.keywords = { include: keywords };
|
|
178
|
+
const geo = readPeopleSearchGeo(options);
|
|
179
|
+
if (geo)
|
|
180
|
+
filters.geo = geo;
|
|
181
|
+
const minConnections = readNonNegativeInt(options.minConnections);
|
|
182
|
+
if (minConnections !== undefined)
|
|
183
|
+
filters.min_connections = minConnections;
|
|
184
|
+
const contactability = {};
|
|
185
|
+
if (options.requireEmail)
|
|
186
|
+
contactability.require_work_email = true;
|
|
187
|
+
if (options.requirePhone)
|
|
188
|
+
contactability.require_mobile = true;
|
|
189
|
+
if (Object.keys(contactability).length > 0)
|
|
190
|
+
filters.contactability = contactability;
|
|
191
|
+
const company = {};
|
|
192
|
+
const companyDomains = readCsvOption(options.companyDomains);
|
|
193
|
+
if (companyDomains.length > 0)
|
|
194
|
+
company.domains = { include: companyDomains };
|
|
195
|
+
const companyNames = readCsvOption(options.companyNames);
|
|
196
|
+
if (companyNames.length > 0)
|
|
197
|
+
company.names = { include: companyNames };
|
|
198
|
+
const employees = readRangeOption(options.employees, "--employees");
|
|
199
|
+
if (employees)
|
|
200
|
+
company.employee_count = employees;
|
|
201
|
+
if (Object.keys(company).length > 0)
|
|
202
|
+
filters.company = company;
|
|
203
|
+
const maxPerCompany = readPositiveInt(options.maxPerCompany);
|
|
204
|
+
if (maxPerCompany !== undefined)
|
|
205
|
+
filters.max_results_per_company = maxPerCompany;
|
|
206
|
+
const explicit = options.filtersJson ? parseJsonObject(readFileIfPresent(options.filtersJson)) : null;
|
|
207
|
+
if (explicit) {
|
|
208
|
+
for (const [key, value] of Object.entries(explicit)) {
|
|
209
|
+
filters[key] = value;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
return Object.keys(filters).length > 0 ? filters : null;
|
|
213
|
+
}
|
|
214
|
+
// Person location filters share one `geo` object: country, continent, and sales
|
|
215
|
+
// region are separate provider fields, so each one must merge into it rather than
|
|
216
|
+
// replace it.
|
|
217
|
+
function readPeopleSearchGeo(options) {
|
|
218
|
+
const geo = {};
|
|
219
|
+
const countries = readCsvOption(options.countries);
|
|
220
|
+
if (countries.length > 0)
|
|
221
|
+
geo.countries = countries;
|
|
222
|
+
const continents = readCsvOption(options.continents);
|
|
223
|
+
if (continents.length > 0)
|
|
224
|
+
geo.continents = continents;
|
|
225
|
+
const salesRegions = readCsvOption(options.salesRegions);
|
|
226
|
+
if (salesRegions.length > 0)
|
|
227
|
+
geo.sales_regions = salesRegions;
|
|
228
|
+
return Object.keys(geo).length > 0 ? geo : null;
|
|
229
|
+
}
|
|
230
|
+
export function registerPeopleFamily(program, handleAsyncAction) {
|
|
231
|
+
program
|
|
232
|
+
.command("people")
|
|
233
|
+
.description("People and contact prospecting workflows.")
|
|
234
|
+
.addCommand(new Command("search")
|
|
235
|
+
.description("Plan, dry-run, or queue provider-backed people/contact search.")
|
|
236
|
+
.addCommand(new Command("filters")
|
|
237
|
+
.description("List Find people filter fields and options without a provider call: people.* (title, level, function, location, education) and company.* (industry, headcount, location, funding, up to 50 company LinkedIn URLs). Every enum field ships its full accepted-value list, so no value has to be guessed. Shape rule: only a field whose own key ends in .include or .exclude takes {include, exclude} — every other list field takes the strings directly. For the companies in a table you already have, sample them free with people search preview --from-table <table> --linkedin-url-column <key>, then people search plan|run --from-table to search each company and write a company relation back.")
|
|
238
|
+
.option("--json", "Print a JSON envelope.")
|
|
239
|
+
.action(async (options) => {
|
|
240
|
+
await handleAsyncAction("people search filters", options, () => requestOxygen("/api/cli/people/search/preview"));
|
|
241
|
+
}))
|
|
242
|
+
.addCommand(new Command("preview")
|
|
243
|
+
.description("Preview up to 50 people free, using native filters from people search filters. For the companies in one of your tables, pass --from-table with --linkedin-url-column; for a few named companies, put their LinkedIn company URLs in company.linkedin_url. Totals, per-company counts, company_fit (what a person row cannot tell you about fit) and the priced next step come first, the rows last. 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.")
|
|
244
|
+
.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.')
|
|
245
|
+
.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.")
|
|
246
|
+
.option("--linkedin-url-column <key>", "Column of --from-table that holds each company's LinkedIn URL.")
|
|
247
|
+
.option("--json", "Print a JSON envelope.")
|
|
248
|
+
.action(async (options) => {
|
|
249
|
+
await handleAsyncAction("people search preview", options, async () => {
|
|
250
|
+
if ((options.fromTable === undefined) !== (options.linkedinUrlColumn === undefined)) {
|
|
251
|
+
throw new OxygenError("invalid_request", "--from-table and --linkedin-url-column go together.", { exitCode: 1 });
|
|
252
|
+
}
|
|
253
|
+
return requestOxygen("/api/cli/people/search/preview", {
|
|
254
|
+
method: "POST",
|
|
255
|
+
body: {
|
|
256
|
+
filters: parseJsonObject(readFileIfPresent(options.filtersJson)),
|
|
257
|
+
...(options.fromTable !== undefined
|
|
258
|
+
? { source: { table: options.fromTable, linkedin_url_column: options.linkedinUrlColumn } }
|
|
259
|
+
: {}),
|
|
260
|
+
},
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
}))
|
|
264
|
+
.addCommand(new Command("plan")
|
|
265
|
+
.description("Compile a people-search prompt and optional typed persona filters into ordered provider routes without provider calls.")
|
|
266
|
+
.option("--prompt <text-or-file>", "People-search prompt, or a path to a prompt file. Required unless --from-table names the companies to source employees from.")
|
|
267
|
+
.option("--target-count <n>", "Desired contact count. Omit it to plan every match the live route can reach, up to a tenth of the spendable credits. Single-plan ceiling 50,000; larger requests return an explicit clamp warning and segmentation guidance.")
|
|
268
|
+
.option("--source-intent <intent>", "Override detected intent: persona_search, account_contacts, audience_sizing, profile_lookup, concept_persona, or fallback_broad.")
|
|
269
|
+
.option("--filters-json <json-or-file>", "PeopleSearchFilters JSON inline or a @file/path; wins over individual flags per top-level filter path.")
|
|
270
|
+
.option("--titles <csv>", "Comma-separated job titles to include.")
|
|
271
|
+
.option("--title-match <mode>", "How to match --titles: keyword (default, full-text/partial, highest recall) or exact (literal title only).")
|
|
272
|
+
.option("--adjacent-titles <csv>", "Comma-separated adjacent/looser titles to accept.")
|
|
273
|
+
.option("--exclude-titles <csv>", "Comma-separated job titles to exclude.")
|
|
274
|
+
.option("--seniorities <csv>", "Comma-separated seniority levels: C-Suite, VP, Director, Manager, Staff.")
|
|
275
|
+
.option("--departments <csv>", "Comma-separated departments/functions: Sales, Marketing, Engineering, ...")
|
|
276
|
+
.option("--job-functions <csv>", PEOPLE_SEARCH_JOB_FUNCTIONS_HELP)
|
|
277
|
+
.option("--countries <csv>", "Comma-separated ISO 3166-1 alpha-2 person country codes.")
|
|
278
|
+
.option("--continents <csv>", PEOPLE_SEARCH_CONTINENTS_HELP)
|
|
279
|
+
.option("--sales-regions <csv>", PEOPLE_SEARCH_SALES_REGIONS_HELP)
|
|
280
|
+
.option("--min-connections <n>", PEOPLE_SEARCH_MIN_CONNECTIONS_HELP)
|
|
281
|
+
.option("--from-table <table>", PEOPLE_SEARCH_FROM_TABLE_HELP)
|
|
282
|
+
.option("--linkedin-url-column <key>", PEOPLE_SEARCH_LINKEDIN_COLUMN_HELP)
|
|
283
|
+
.option("--company-offset <n>", PEOPLE_SEARCH_COMPANY_OFFSET_HELP)
|
|
284
|
+
.option("--company-limit <n>", PEOPLE_SEARCH_COMPANY_LIMIT_HELP)
|
|
285
|
+
.option("--keywords <csv>", "Comma-separated free-text persona keywords.")
|
|
286
|
+
.option("--company-domains <csv>", "Comma-separated company domains to scope contacts to.")
|
|
287
|
+
.option("--company-names <csv>", "Comma-separated company names to scope contacts to.")
|
|
288
|
+
.option("--employees <range>", "Company employee count range: 20-200, 500+, or -50.")
|
|
289
|
+
.option("--require-email", "Only return people with a work email available (provider-dependent).")
|
|
290
|
+
.option("--require-phone", "Only return people with a phone/mobile available (provider-dependent).")
|
|
291
|
+
.option("--max-per-company <n>", "Cap on contacts per company for account-anchored searches.")
|
|
292
|
+
.option("--estimate", "Run a free count probe for an estimated match count (on by default).")
|
|
293
|
+
.option("--no-estimate", "Skip the default free count probe (people-search sizing is on by default).")
|
|
294
|
+
.option("--materialize-preview", "Create a preview table with route rows.")
|
|
295
|
+
.option("--json", "Print a JSON envelope.")
|
|
296
|
+
.action(async (options) => {
|
|
297
|
+
await handleAsyncAction("people search plan", options, () => requestOxygen("/api/cli/people/search/plan", {
|
|
298
|
+
method: "POST",
|
|
299
|
+
body: readPeopleSearchPlanBody(options),
|
|
300
|
+
}));
|
|
301
|
+
}))
|
|
302
|
+
.addCommand(new Command("run")
|
|
303
|
+
.description("Return a dry-run request or queue a live people-search ingestion run. Upsert dedup is on linkedin_url.")
|
|
304
|
+
.option("--source <source>", "Use people_search with --source-filters-json instead of a prompt or plan: the same filters as people search preview, no prompt needed.")
|
|
305
|
+
.option("--source-filters-json <json-or-file>", 'Same nested filters as the free preview, e.g. {"people":{"job_title":{"include":["VP Sales"]}}}; use with --source people_search.')
|
|
306
|
+
.option("--table-name <name>", "Name the table this run creates. Ignored when --table names an existing table, which must already exist.")
|
|
307
|
+
.option("--prompt <text-or-file>", "People-search prompt, or a path to a prompt file.")
|
|
308
|
+
.option("--plan-json <json-or-file>", "Plan JSON returned by people search plan, or a path to a JSON file.")
|
|
309
|
+
.option("--route-id <id>", "Route id from the plan to execute.")
|
|
310
|
+
.option("--tool-id <tool>", "Tool id from the plan to execute.")
|
|
311
|
+
.option("--table <table>", "Existing table id or slug to receive rows. If omitted for live, Oxygen creates a table.")
|
|
312
|
+
.option("--project <project>", "Folder (project) id or slug the created table lands in. Ignored with --table. Defaults to the workspace default folder.")
|
|
313
|
+
.option("--upsert-key <column>", "Column key used for live upsert. Must match the plan upsert key (linkedin_url).")
|
|
314
|
+
.option("--mode <mode>", "dry_run or live. Defaults to dry_run.")
|
|
315
|
+
.option("--max-pages <n>", "Maximum provider pages to ingest. Defaults to the route estimate. With --from-table it means pages per company.")
|
|
316
|
+
.option("--max-credits <n>", "Required credit ceiling for live runs.")
|
|
317
|
+
.option("--target-count <n>", "How many people to source. Omit it to take every match, up to what one query reaches and a tenth of the spendable credits. Single-plan ceiling 50,000.")
|
|
318
|
+
.option("--source-intent <intent>", "Override detected intent when planning from --prompt.")
|
|
319
|
+
.option("--filters-json <json-or-file>", "PeopleSearchFilters JSON inline or a @file/path when planning from --prompt; wins over individual flags per top-level filter path.")
|
|
320
|
+
.option("--titles <csv>", "Comma-separated job titles to include when planning from --prompt.")
|
|
321
|
+
.option("--title-match <mode>", "How to match --titles: keyword (default, full-text/partial, highest recall) or exact (literal title only).")
|
|
322
|
+
.option("--adjacent-titles <csv>", "Comma-separated adjacent titles when planning from --prompt.")
|
|
323
|
+
.option("--exclude-titles <csv>", "Comma-separated job titles to exclude when planning from --prompt.")
|
|
324
|
+
.option("--seniorities <csv>", "Comma-separated seniority levels when planning from --prompt.")
|
|
325
|
+
.option("--departments <csv>", "Comma-separated departments/functions when planning from --prompt.")
|
|
326
|
+
.option("--job-functions <csv>", PEOPLE_SEARCH_JOB_FUNCTIONS_HELP)
|
|
327
|
+
.option("--countries <csv>", "Comma-separated ISO 3166-1 alpha-2 person country codes when planning from --prompt.")
|
|
328
|
+
.option("--continents <csv>", PEOPLE_SEARCH_CONTINENTS_HELP)
|
|
329
|
+
.option("--sales-regions <csv>", PEOPLE_SEARCH_SALES_REGIONS_HELP)
|
|
330
|
+
.option("--min-connections <n>", PEOPLE_SEARCH_MIN_CONNECTIONS_HELP)
|
|
331
|
+
.option("--from-table <table>", PEOPLE_SEARCH_FROM_TABLE_HELP)
|
|
332
|
+
.option("--linkedin-url-column <key>", PEOPLE_SEARCH_LINKEDIN_COLUMN_HELP)
|
|
333
|
+
.option("--company-offset <n>", PEOPLE_SEARCH_COMPANY_OFFSET_HELP)
|
|
334
|
+
.option("--company-limit <n>", PEOPLE_SEARCH_COMPANY_LIMIT_HELP)
|
|
335
|
+
.option("--pages-per-company <n>", "Alias for --max-pages on a --from-table run: provider pages per company, 50 people per page.")
|
|
336
|
+
.option("--keywords <csv>", "Comma-separated persona keywords when planning from --prompt.")
|
|
337
|
+
.option("--company-domains <csv>", "Comma-separated company domains when planning from --prompt.")
|
|
338
|
+
.option("--company-names <csv>", "Comma-separated company names when planning from --prompt.")
|
|
339
|
+
.option("--employees <range>", "Company employee count range when planning from --prompt.")
|
|
340
|
+
.option("--require-email", "Only return people with a work email available when planning from --prompt.")
|
|
341
|
+
.option("--require-phone", "Only return people with a phone available when planning from --prompt.")
|
|
342
|
+
.option("--max-per-company <n>", "Cap on contacts per company when planning from --prompt.")
|
|
343
|
+
.option("--estimate", "Run a free count probe when planning from --prompt (on by default).")
|
|
344
|
+
.option("--no-estimate", "Skip the default free count probe when planning from --prompt.")
|
|
345
|
+
.option("--approved", "Required for live runs after inspecting dry-run output.")
|
|
346
|
+
.option("--json", "Print a JSON envelope.")
|
|
347
|
+
.action(async (options) => {
|
|
348
|
+
await handleAsyncAction("people search run", options, () => requestOxygen("/api/cli/people/search/run", {
|
|
349
|
+
method: "POST",
|
|
350
|
+
body: readPeopleSearchRunBody(options),
|
|
351
|
+
}));
|
|
352
|
+
})));
|
|
353
|
+
}
|
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import { parseJsonArray, readFileIfPresent } from "../command-support.js";
|
|
2
|
+
import { requestOxygen } from "../http-client.js";
|
|
3
|
+
import { readOption } from "../util.js";
|
|
4
|
+
import { OxygenError } from "@oxygen/shared/cli-result";
|
|
5
|
+
import { LINKEDIN_SENDER_LIMIT_DEFAULTS, LINKEDIN_SENDER_LIMIT_MAXIMUMS } from "@oxygen/shared/linkedin-sequences";
|
|
6
|
+
import { Command } from "commander";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { resolve } from "node:path";
|
|
9
|
+
// Shared query string for `posts comments` / `posts reactions` (post social_id +
|
|
10
|
+
// optional comment scope, pagination, sort, and account selection).
|
|
11
|
+
function buildPostEngagementQuery(options) {
|
|
12
|
+
const params = new URLSearchParams();
|
|
13
|
+
params.set("post", readOption(options.post) ?? "");
|
|
14
|
+
const commentId = readOption(options.commentId);
|
|
15
|
+
if (commentId)
|
|
16
|
+
params.set("comment_id", commentId);
|
|
17
|
+
const sortBy = readOption(options.sortBy);
|
|
18
|
+
if (sortBy)
|
|
19
|
+
params.set("sort_by", sortBy);
|
|
20
|
+
const cursor = readOption(options.cursor);
|
|
21
|
+
if (cursor)
|
|
22
|
+
params.set("cursor", cursor);
|
|
23
|
+
const limit = readOption(options.limit);
|
|
24
|
+
if (limit)
|
|
25
|
+
params.set("limit", limit);
|
|
26
|
+
const account = readOption(options.account);
|
|
27
|
+
if (account)
|
|
28
|
+
params.set("account", account);
|
|
29
|
+
return params.toString();
|
|
30
|
+
}
|
|
31
|
+
export function registerPostsFamily(program, handleAsyncAction) {
|
|
32
|
+
program.addCommand(new Command("posts")
|
|
33
|
+
.description("Read and publish LinkedIn posts through a connected account. `get`, `comments`, and `reactions` are LIVE provider reads: they use 0 Oxygen credits but consume the sender's daily account-read allowance. Do not use them when a task forbids provider calls; use `publishing comments list` for the local Community queue, which discovers recent posts owned by connected accounts in the background. `create` publishes a real post (approval-gated). The direct commands here address ONE post you already have an id for; aggregate analytics are `oxygen publishing analytics` (LinkedIn includes owned posts discovered from the provider).")
|
|
34
|
+
.addCommand(new Command("get")
|
|
35
|
+
.description("LIVE provider read of one LinkedIn post. Uses 0 Oxygen credits but consumes the sender's metered account-read allowance; do not run it when the task forbids provider calls. Returns the post and its composite social_id (reuse that social_id for `posts comments`, `posts reactions`, and `engagement harvest --source unipile` — NOT the raw activity URN). This does not populate or refresh the durable `publishing comments` queue.")
|
|
36
|
+
.requiredOption("--post <id>", "Numeric activity id, activity URL, or composite social_id. For a direct live read, take the id from the post's LinkedIn URL or a scheduled post (`oxygen publishing posts get <post_id>` → provider_post_id); Community discovers recent owned posts separately in the background.")
|
|
37
|
+
.option("--account <ref>", "Sender account to read through (sender id, connection id, or Unipile account id). Omit for the org default.")
|
|
38
|
+
.option("--json", "Print a JSON envelope.")
|
|
39
|
+
.action(async (options) => {
|
|
40
|
+
await handleAsyncAction("posts get", options, () => {
|
|
41
|
+
const params = new URLSearchParams();
|
|
42
|
+
params.set("post", readOption(options.post) ?? "");
|
|
43
|
+
const account = readOption(options.account);
|
|
44
|
+
if (account)
|
|
45
|
+
params.set("account", account);
|
|
46
|
+
return requestOxygen(`/api/cli/linkedin/posts?${params.toString()}`);
|
|
47
|
+
});
|
|
48
|
+
}))
|
|
49
|
+
.addCommand(new Command("comments")
|
|
50
|
+
.description("Read one post's comments live from LinkedIn (or one comment's replies with --comment-id). This consumes the sender's metered account-read allowance, uses 0 Oxygen credits, and does not populate the durable queue. For the zero-provider-call cross-post work queue, use `publishing comments list`. --post MUST be the composite social_id from `posts get`, not the activity URN.")
|
|
51
|
+
.requiredOption("--post <social_id>", "Composite post social_id from `posts get`.")
|
|
52
|
+
.option("--comment-id <id>", "List replies to this comment instead of top-level comments.")
|
|
53
|
+
.option("--sort-by <sort>", "Comment sort order (provider-defined, e.g. recent|relevant).")
|
|
54
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous page.")
|
|
55
|
+
.option("--limit <n>", "Max comments to return this page.")
|
|
56
|
+
.option("--account <ref>", "Sender account to read through. Omit for the org default.")
|
|
57
|
+
.option("--json", "Print a JSON envelope.")
|
|
58
|
+
.action(async (options) => {
|
|
59
|
+
await handleAsyncAction("posts comments", options, () => requestOxygen(`/api/cli/linkedin/posts/comments?${buildPostEngagementQuery(options)}`));
|
|
60
|
+
}))
|
|
61
|
+
.addCommand(new Command("reactions")
|
|
62
|
+
.description("List a post's reactions (or a comment's reactions with --comment-id). --post MUST be the composite social_id from `posts get`. Metered as an account read.")
|
|
63
|
+
.requiredOption("--post <social_id>", "Composite post social_id from `posts get`.")
|
|
64
|
+
.option("--comment-id <id>", "List reactions to this comment instead of the post.")
|
|
65
|
+
.option("--cursor <cursor>", "Pagination cursor from a previous page.")
|
|
66
|
+
.option("--limit <n>", "Max reactions to return this page.")
|
|
67
|
+
.option("--account <ref>", "Sender account to read through. Omit for the org default.")
|
|
68
|
+
.option("--json", "Print a JSON envelope.")
|
|
69
|
+
.action(async (options) => {
|
|
70
|
+
await handleAsyncAction("posts reactions", options, () => requestOxygen(`/api/cli/linkedin/posts/reactions?${buildPostEngagementQuery(options)}`));
|
|
71
|
+
}))
|
|
72
|
+
.addCommand(new Command("create")
|
|
73
|
+
.description(`Publish a LinkedIn post from the connected account. A REAL public write, so it prints a preview by default and only publishes with --approved. Counts against the account's posts_per_day cap (default ${LINKEDIN_SENDER_LIMIT_DEFAULTS.posts_per_day}, max ${LINKEDIN_SENDER_LIMIT_MAXIMUMS.posts_per_day}), not the daily action total, and is exempt from working hours and action spacing. (Company-page posting is not supported — Unipile exposes it only via the fragile raw route.)`)
|
|
74
|
+
.option("--text <text>", "Post body text.")
|
|
75
|
+
.option("--text-file <path>", "Read the post body from a file (alternative to --text).")
|
|
76
|
+
.option("--account <ref>", "Sender account to post from. Omit for the org default.")
|
|
77
|
+
.option("--as-organization <id>", "Publish as a company page the account administers (advanced; may require raw-route support).")
|
|
78
|
+
.option("--repost <id>", "Quote or share this LinkedIn post.")
|
|
79
|
+
.option("--include-job-posting <id>", "Embed a LinkedIn job preview.")
|
|
80
|
+
.option("--attachments-json <json-or-file>", "Up to 20 {content:<base64>,content_type:<MIME>,filename:<name>} objects; use a local JSON file for media.")
|
|
81
|
+
.option("--external-link <url>", "Attach an external link to the post.")
|
|
82
|
+
.option("--approved", "Publish for real. Without this flag, prints a preview only.")
|
|
83
|
+
.option("--json", "Print a JSON envelope.")
|
|
84
|
+
.action(async (options) => {
|
|
85
|
+
await handleAsyncAction("posts create", options, () => {
|
|
86
|
+
const inlineText = readOption(options.text);
|
|
87
|
+
const textFile = readOption(options.textFile);
|
|
88
|
+
const text = inlineText ?? (textFile ? readFileSync(resolve(textFile), "utf8") : undefined);
|
|
89
|
+
if (!text || !text.trim())
|
|
90
|
+
throw new Error("--text or --text-file is required to publish a post.");
|
|
91
|
+
const account = readOption(options.account);
|
|
92
|
+
const asOrganization = readOption(options.asOrganization);
|
|
93
|
+
const externalLink = readOption(options.externalLink);
|
|
94
|
+
return requestOxygen("/api/cli/linkedin/posts", {
|
|
95
|
+
method: "POST",
|
|
96
|
+
body: {
|
|
97
|
+
text,
|
|
98
|
+
...(account ? { account } : {}),
|
|
99
|
+
...(asOrganization ? { as_organization: asOrganization } : {}),
|
|
100
|
+
...(externalLink ? { external_link: externalLink } : {}),
|
|
101
|
+
...(readOption(options.repost) ? { repost: readOption(options.repost) } : {}),
|
|
102
|
+
...(readOption(options.includeJobPosting) ? { include_job_posting: readOption(options.includeJobPosting) } : {}),
|
|
103
|
+
...(options.attachmentsJson !== undefined ? { attachments: parseJsonArray(readFileIfPresent(options.attachmentsJson)) } : {}),
|
|
104
|
+
...(options.approved ? { approved: true } : {}),
|
|
105
|
+
},
|
|
106
|
+
});
|
|
107
|
+
});
|
|
108
|
+
}))
|
|
109
|
+
.addCommand(new Command("delete")
|
|
110
|
+
.description("Delete a LinkedIn post the connected account authored — a REAL, irreversible public write. Refuses without --approved (exit 7). --post is the composite social_id returned when the post was created (or by `oxygen posts get`), NOT the activity URN. To remove only the OXYGEN copy, including a settled published copy, while keeping LinkedIn intact, use `oxygen publishing posts delete`.")
|
|
111
|
+
.requiredOption("--post <social_id>", "Composite post social_id from `oxygen posts get` (the id returned when the post was created).")
|
|
112
|
+
.option("--account <ref>", "Sender account that authored the post (sender id, connection id, or Unipile account id). Omit for the org default.")
|
|
113
|
+
.option("--approved", "Actually delete the post. Without it the command refuses and deletes nothing.")
|
|
114
|
+
.option("--json", "Print a JSON envelope.")
|
|
115
|
+
.action(async (options) => {
|
|
116
|
+
await handleAsyncAction("posts delete", options, () => {
|
|
117
|
+
const post = readOption(options.post);
|
|
118
|
+
if (!post)
|
|
119
|
+
throw new Error("--post is required (the composite social_id from `oxygen posts get`).");
|
|
120
|
+
const account = readOption(options.account);
|
|
121
|
+
// The DELETE route has no preview mode — it removes the live post on
|
|
122
|
+
// the first call — so the approval gate lives here. Refuse rather than
|
|
123
|
+
// silently deleting a real, public, irreversible artifact.
|
|
124
|
+
if (!options.approved) {
|
|
125
|
+
throw new OxygenError("approval_required", "Deleting a LinkedIn post is a real, irreversible public write. Re-run with --approved to delete it.", {
|
|
126
|
+
details: { post, ...(account ? { account } : {}) },
|
|
127
|
+
exitCode: 7,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const params = new URLSearchParams();
|
|
131
|
+
params.set("post", post);
|
|
132
|
+
if (account)
|
|
133
|
+
params.set("account", account);
|
|
134
|
+
return requestOxygen(`/api/cli/linkedin/posts?${params.toString()}`, {
|
|
135
|
+
method: "DELETE",
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
})));
|
|
139
|
+
}
|