@oxygen-agent/cli 1.1014.1138 → 1.1014.1221

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +2 -0
  3. package/dist/command-support.d.ts +8 -0
  4. package/dist/command-support.js +11 -0
  5. package/dist/commands/api-keys.d.ts +9 -0
  6. package/dist/commands/api-keys.js +4 -1
  7. package/dist/commands/capabilities.js +4 -2
  8. package/dist/commands/crm.js +28 -2
  9. package/dist/commands/dashboards.js +2 -2
  10. package/dist/commands/db.js +34 -0
  11. package/dist/commands/find.js +1 -1
  12. package/dist/commands/knowledge.js +2 -0
  13. package/dist/commands/mailboxes.js +3 -2
  14. package/dist/commands/messages.js +6 -1
  15. package/dist/commands/senders.js +2 -0
  16. package/dist/commands/sequences.js +1 -1
  17. package/dist/commands/signals.js +11 -2
  18. package/dist/deliverability-evidence-commands.d.ts +4 -0
  19. package/dist/deliverability-evidence-commands.js +104 -0
  20. package/dist/diagnostic-context.d.ts +21 -0
  21. package/dist/diagnostic-context.js +25 -0
  22. package/dist/diagnostics.d.ts +1 -3
  23. package/dist/diagnostics.js +12 -30
  24. package/dist/http-client.js +1 -1
  25. package/dist/network-mining-host.d.ts +6 -0
  26. package/dist/network-mining-host.js +10 -0
  27. package/node_modules/@oxygen/formula/dist/value-normalizers.d.ts +1 -1
  28. package/node_modules/@oxygen/formula/dist/value-normalizers.js +5 -0
  29. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +169 -0
  30. package/node_modules/@oxygen/shared/dist/agent-sessions.js +17 -0
  31. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  32. package/node_modules/@oxygen/shared/dist/capability-discovery.js +114 -5
  33. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
  34. package/node_modules/@oxygen/shared/dist/contracts/operation-request.d.ts +92 -0
  35. package/node_modules/@oxygen/shared/dist/contracts/operation-request.js +36 -0
  36. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +3 -90
  37. package/node_modules/@oxygen/shared/dist/contracts/operations.js +5 -39
  38. package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.d.ts +2 -0
  39. package/node_modules/@oxygen/shared/dist/contracts/reply-oxygent.js +123 -0
  40. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +1 -0
  41. package/node_modules/@oxygen/shared/dist/crm-activity-events.d.ts +4 -0
  42. package/node_modules/@oxygen/shared/dist/crm-activity-events.js +1 -0
  43. package/node_modules/@oxygen/shared/dist/crm-record-brief.d.ts +10 -0
  44. package/node_modules/@oxygen/shared/dist/crm-record-brief.js +22 -0
  45. package/node_modules/@oxygen/shared/dist/deliverability-blacklist.d.ts +74 -0
  46. package/node_modules/@oxygen/shared/dist/deliverability-blacklist.js +118 -0
  47. package/node_modules/@oxygen/shared/dist/deliverability-checks.d.ts +138 -0
  48. package/node_modules/@oxygen/shared/dist/deliverability-checks.js +124 -0
  49. package/node_modules/@oxygen/shared/dist/deliverability-evidence.d.ts +366 -0
  50. package/node_modules/@oxygen/shared/dist/deliverability-evidence.js +169 -0
  51. package/node_modules/@oxygen/shared/dist/deliverability-loop.d.ts +334 -0
  52. package/node_modules/@oxygen/shared/dist/deliverability-loop.js +312 -0
  53. package/node_modules/@oxygen/shared/dist/deliverability-surface.d.ts +42 -0
  54. package/node_modules/@oxygen/shared/dist/deliverability-surface.js +66 -0
  55. package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +34 -4
  56. package/node_modules/@oxygen/shared/dist/domain-authentication.d.ts +24 -0
  57. package/node_modules/@oxygen/shared/dist/domain-authentication.js +20 -0
  58. package/node_modules/@oxygen/shared/dist/file-import.js +11 -3
  59. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.d.ts +16 -0
  60. package/node_modules/@oxygen/shared/dist/linkedin-connection-identity.js +30 -2
  61. package/node_modules/@oxygen/shared/dist/network-mining.d.ts +5 -0
  62. package/node_modules/@oxygen/shared/dist/network-mining.js +9 -0
  63. package/node_modules/@oxygen/shared/dist/received-authentication.d.ts +74 -0
  64. package/node_modules/@oxygen/shared/dist/received-authentication.js +452 -0
  65. package/node_modules/@oxygen/shared/dist/recipient-provider.d.ts +21 -0
  66. package/node_modules/@oxygen/shared/dist/recipient-provider.js +33 -0
  67. package/node_modules/@oxygen/shared/dist/reply-oxygent.d.ts +73 -0
  68. package/node_modules/@oxygen/shared/dist/reply-oxygent.js +31 -0
  69. package/node_modules/@oxygen/shared/dist/repricing.d.ts +10 -1
  70. package/node_modules/@oxygen/shared/dist/repricing.js +16 -4
  71. package/node_modules/@oxygen/shared/dist/research-findings.d.ts +80 -0
  72. package/node_modules/@oxygen/shared/dist/research-findings.js +199 -0
  73. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.d.ts +2 -0
  74. package/node_modules/@oxygen/shared/dist/sequence-internals/esp-policy.js +4 -0
  75. package/node_modules/@oxygen/shared/dist/sequences.d.ts +6 -0
  76. package/node_modules/@oxygen/shared/dist/sequences.js +8 -0
  77. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +20 -3
  78. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  79. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  80. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.d.ts +8 -2
  81. package/node_modules/@oxygen/shared/dist/workspace-event-catalog.js +63 -0
  82. package/package.json +1 -1
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.1014.1138
37
+ Version: 1.1014.1221
@@ -107,6 +107,8 @@ const MUTATING_VERBS = new Set([
107
107
  const MUTATING_COMMANDS = new Set([
108
108
  // Live DNS checks save observations; --cached explicitly reads saved evidence.
109
109
  "domains dns",
110
+ // Stores one copy-score check run (0 credits, nothing sent); the noun leaf would read as a read.
111
+ "mailboxes deliverability score-copy",
110
112
  // Sends the workspace's company documents to Twilio for a human review that
111
113
  // cannot be edited once started. `submit` is its only leaf, so it is exact.
112
114
  "voice verification submit",
@@ -1,3 +1,4 @@
1
+ import { Option } from "commander";
1
2
  import { JsonOptions, handleAsyncAction } from "./cli-output.js";
2
3
  import { type CliProfileSource } from "./cli-profile-context.js";
3
4
  import { type ProfileResolution, type StoredCredentials, type StoredProfileIdentity, type StoredOrganization } from "./credentials.js";
@@ -91,7 +92,14 @@ export type CrmSignalRecordOptions = JsonOptions & {
91
92
  export type CrmLeadsTodayOptions = JsonOptions & {
92
93
  limit?: string;
93
94
  withinDays?: string;
95
+ rank?: string;
94
96
  };
97
+ /**
98
+ * `--rank focus` (Network Mining's warmth → fit → decayed-intent rank). Hidden
99
+ * while Network Mining is DEV-only: production help must not offer a flag its
100
+ * API refuses (focus_rank_unavailable). Unhide with the production rollout.
101
+ */
102
+ export declare function leadsTodayRankOption(): Option;
95
103
  export type ContextProfileUpdateOptions = JsonOptions & {
96
104
  dataJson?: string;
97
105
  summary?: string;
@@ -1,3 +1,4 @@
1
+ import { Option } from "commander";
1
2
  import { emitCliFailure, writeJson } from "./cli-output.js";
2
3
  import { cliProfileSource } from "./cli-profile-context.js";
3
4
  import { parseJsonObject, readNonNegativeInt, readPositiveInt, readRecordString } from "./cli-values.js";
@@ -58,6 +59,14 @@ export function readCollabPath(value) {
58
59
  const raw = readOption(value);
59
60
  return raw ? formatSubjectPath(parseSubjectPath(raw)) : undefined;
60
61
  }
62
+ /**
63
+ * `--rank focus` (Network Mining's warmth → fit → decayed-intent rank). Hidden
64
+ * while Network Mining is DEV-only: production help must not offer a flag its
65
+ * API refuses (focus_rank_unavailable). Unhide with the production rollout.
66
+ */
67
+ export function leadsTodayRankOption() {
68
+ return new Option("--rank <rank>", "signal (default) or focus: warmth, then ICP fit, then decayed 90-day inbound intent, each lead with reasons, evidence ids and the teammates who know them. Only where Network Mining is enabled; elsewhere the API answers focus_rank_unavailable.").hideHelp();
69
+ }
61
70
  // One cadence vocabulary across every scheduled surface (column refresh, table
62
71
  // feed): `translateScheduleEvery` in apps/web resolves all four forms, so this
63
72
  // string is the single place the CLI spells them out.
@@ -264,6 +273,8 @@ export function buildCrmLeadsTodayPath(options) {
264
273
  params.set("limit", options.limit);
265
274
  if (options.withinDays)
266
275
  params.set("within_days", options.withinDays);
276
+ if (options.rank)
277
+ params.set("rank", options.rank);
267
278
  const query = params.toString();
268
279
  return query ? `/api/cli/crm/signals/leads-today?${query}` : "/api/cli/crm/signals/leads-today";
269
280
  }
@@ -1,3 +1,12 @@
1
+ import { JsonOptions } from "../cli-output.js";
1
2
  import { ExtractedCommandActionHandler } from "../command-support.js";
2
3
  import { Command } from "commander";
4
+ type ApiKeysCreateOptions = JsonOptions & {
5
+ name?: string;
6
+ expiresAt?: string;
7
+ expiresInDays?: string;
8
+ readOnly?: boolean;
9
+ };
10
+ export declare function buildApiKeyCreateBody(options: ApiKeysCreateOptions): Record<string, unknown>;
3
11
  export declare function registerApiKeysFamily(program: Command, handleAsyncAction: ExtractedCommandActionHandler): void;
12
+ export {};
@@ -3,7 +3,7 @@ import { requestOxygen } from "../http-client.js";
3
3
  import { readOption } from "../util.js";
4
4
  import { OxygenError } from "@oxygen/shared/cli-result";
5
5
  import { Command } from "commander";
6
- function buildApiKeyCreateBody(options) {
6
+ export function buildApiKeyCreateBody(options) {
7
7
  const body = {};
8
8
  const name = readOption(options.name);
9
9
  if (name)
@@ -11,6 +11,8 @@ function buildApiKeyCreateBody(options) {
11
11
  const expiresAt = resolveApiKeyExpiresAt(options);
12
12
  if (expiresAt)
13
13
  body.expires_at = expiresAt;
14
+ if (options.readOnly)
15
+ body.access = "read_only";
14
16
  return body;
15
17
  }
16
18
  function resolveApiKeyExpiresAt(options) {
@@ -58,6 +60,7 @@ export function registerApiKeysFamily(program, handleAsyncAction) {
58
60
  .option("--name <name>", "Key name. Defaults to 'CLI key'.")
59
61
  .option("--expires-at <iso>", "Expiration timestamp as ISO 8601.")
60
62
  .option("--expires-in-days <days>", "Expire the key after this many days.")
63
+ .option("--read-only", "Mint a key that can read but never change the workspace.")
61
64
  .option("--json", "Print a JSON envelope.")
62
65
  .action(async (options) => {
63
66
  await handleAsyncAction("api-keys create", options, () => requestOxygen("/api/cli/api-keys", {
@@ -1,4 +1,5 @@
1
- import { OXYGEN_CAPABILITY_ROUTES, getCapabilityRouteMatch, serializeCapabilityRoute } from "@oxygen/shared/capability-discovery";
1
+ import { OXYGEN_CAPABILITY_ROUTES, getCapabilityRouteMatch, serializeCapabilityRoute, withRecordBrief } from "@oxygen/shared/capability-discovery";
2
+ import { networkMiningHostServed } from "../network-mining-host.js";
2
3
  import { OxygenError } from "@oxygen/shared/cli-result";
3
4
  import { inferUserCapabilityRoute } from "@oxygen/shared/user-capability-routing";
4
5
  import { capabilityDiscoveryRecovery } from "@oxygen/shared/user-capability-routing";
@@ -14,7 +15,8 @@ export function registerCapabilitiesFamily(program, handleAsyncAction) {
14
15
  .action(async (queryParts, options) => {
15
16
  await handleAsyncAction("capabilities search", options, async () => {
16
17
  const query = queryParts.join(" ");
17
- const route = inferUserCapabilityRoute(query);
18
+ const inferred = inferUserCapabilityRoute(query);
19
+ const route = inferred ? withRecordBrief(inferred, networkMiningHostServed()) : null;
18
20
  return {
19
21
  query,
20
22
  route: serializeCapabilityRoute(route),
@@ -1,9 +1,11 @@
1
1
  import { emitCliFailure, writeJson } from "../cli-output.js";
2
2
  import { parseJsonObject, readPositiveInt, readRecordString, resolveLiveDryRunMode } from "../cli-values.js";
3
- import { ansi, buildCrmLeadsTodayPath, buildCrmSignalRecordBody, clipCell, handleReadActionWithLens, parseJsonArray, parseJsonValue, readCsvOption, readPositiveNumber, renderTextTable, splitCommaList } from "../command-support.js";
3
+ import { ansi, buildCrmLeadsTodayPath, leadsTodayRankOption, buildCrmSignalRecordBody, clipCell, handleReadActionWithLens, parseJsonArray, parseJsonValue, readCsvOption, readPositiveNumber, renderTextTable, splitCommaList } from "../command-support.js";
4
4
  import { addContractFlags, readContractFlags } from "../contract-flags.js";
5
5
  import { readCrmNoteEditFields, readCrmPhotoFields, readCrmFileLabel, buildCrmObjectsPath, buildCrmConnectionsPath, buildCrmRecordPath } from "../crm-support-flags.js";
6
6
  import { requestOxygen } from "../http-client.js";
7
+ import { networkMiningHostServed } from "../network-mining-host.js";
8
+ import { CRM_RECORD_BRIEF_SECTIONS } from "@oxygen/shared/crm-record-brief";
7
9
  import { isRecord, readClearableOption, readOption } from "../util.js";
8
10
  import { OxygenError, success } from "@oxygen/shared/cli-result";
9
11
  import { CRM_ENRICHMENT_READ_CONTRACT } from "@oxygen/shared/contracts/crm-enrichment-read";
@@ -329,6 +331,17 @@ function buildCrmTimelinePath(object, rowId, options) {
329
331
  const base = `/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/timeline`;
330
332
  return query ? `${base}?${query}` : base;
331
333
  }
334
+ function buildCrmBriefPath(object, record, options) {
335
+ const params = new URLSearchParams();
336
+ const sections = splitCommaList(options.sections);
337
+ if (sections.length > 0)
338
+ params.set("sections", sections.join(","));
339
+ if (options.maxChars)
340
+ params.set("max_chars", options.maxChars);
341
+ const query = params.toString();
342
+ const base = `/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(record)}/brief`;
343
+ return query ? `${base}?${query}` : base;
344
+ }
332
345
  function buildCrmRelatedPath(object, rowId, options) {
333
346
  const params = new URLSearchParams();
334
347
  if (options.previewLimit)
@@ -1257,6 +1270,16 @@ export function registerCrmFamily(program, handleAsyncAction) {
1257
1270
  .action(async (object, rowId, options) => {
1258
1271
  await handleAsyncAction("crm related", options, () => requestOxygen(buildCrmRelatedPath(object, rowId, options)));
1259
1272
  }))
1273
+ .addCommand(new Command("brief")
1274
+ .description("Network Mining (dev): everything stored about one CRM record in one free read — fields, identities, intent signals, teammates connected on LinkedIn, calls and meetings, recent activity, linked conversations and Sequences, notes, open tasks, related records and pinned Knowledge pages. Address the record by row id or any identity (email, domain, LinkedIn URL). Each section reports coverage and freshness; a cut section names the read for the rest.")
1275
+ .argument("<object>", "CRM object slug, such as people or companies.")
1276
+ .argument("<record>", "CRM record row id, an identity value such as sarah@acme.com or acme.com, or the record's exact name.")
1277
+ .option("--sections <list>", `Comma-separated subset: ${CRM_RECORD_BRIEF_SECTIONS.join(", ")}. Default all.`)
1278
+ .option("--max-chars <n>", "Character budget for the whole brief, 3000-20000. Default 8000.")
1279
+ .option("--json", "Print a JSON envelope.")
1280
+ .action(async (object, record, options) => {
1281
+ await handleAsyncAction("crm brief", options, () => requestOxygen(buildCrmBriefPath(object, record, options)));
1282
+ }), { hidden: !networkMiningHostServed() })
1260
1283
  .addCommand(new Command("tag")
1261
1284
  .description("Add/remove workspace tags on one CRM record (Tags primitive — the same vocabulary as sequences, tables, conversations; see `oxygen tags list`). Delta semantics: --add unions, --remove subtracts, remove wins. Standard objects carry the tags attribute after `oxygen crm setup`.")
1262
1285
  .argument("<object>", "CRM object slug, such as companies or people.")
@@ -1341,7 +1364,9 @@ export function registerCrmFamily(program, handleAsyncAction) {
1341
1364
  .description("Log a timeline activity on a CRM record. Defaults to dry-run.")
1342
1365
  .argument("<object>", "CRM object slug, such as companies or people.")
1343
1366
  .argument("<row_id>", "CRM record row id.")
1344
- .requiredOption("--type <type>", "Activity type, such as note, call, meeting_booked, stage_changed.")
1367
+ .requiredOption("--type <type>", networkMiningHostServed()
1368
+ ? "Activity type, such as note, call, meeting_booked, stage_changed. research_finding: --summary is one claim; --metadata-json gives source_kind (message|activity|url) with message_id, activity_id or source_url of this record's evidence, confidence 0-1, observed_at and class (role|interest|stated_need|company|other)."
1369
+ : "Activity type, such as note, call, meeting_booked, stage_changed.")
1345
1370
  .option("--summary <summary>", "Short one-line summary of the activity.")
1346
1371
  .option("--body <body>", "Longer activity body or details.")
1347
1372
  .option("--channel <channel>", "Channel, such as email, linkedin, or phone.")
@@ -1453,6 +1478,7 @@ export function registerCrmFamily(program, handleAsyncAction) {
1453
1478
  .description("List people to contact today, ranked by their most recent GTM intent signal. When the output has rankedBy, equal signals are ordered by the company's ICP fit (see each lead's fit). Do-not-contact filtering is best-effort (email-only leads surface unchecked).")
1454
1479
  .option("--limit <limit>", "Maximum leads to return. Defaults to 25, max 100.")
1455
1480
  .option("--within-days <days>", "Signal look-back window in days. Defaults to 7, max 30.")
1481
+ .addOption(leadsTodayRankOption())
1456
1482
  .option("--json", "Print a JSON envelope.")
1457
1483
  .action(async (options) => {
1458
1484
  await handleAsyncAction("crm signals leads-today", options, () => requestOxygen(buildCrmLeadsTodayPath(options)));
@@ -159,9 +159,9 @@ export function registerDashboardsFamily(program, handleAsyncAction) {
159
159
  await handleAsyncAction("dashboards widget kinds", options, () => requestOxygen("/api/cli/dashboards/widget-kinds"));
160
160
  }))
161
161
  .addCommand(new Command("add")
162
- .description("Add a widget. Kinds: chart, view, iframe, rich_text, code, live_source. The configuration shape depends on the kind — run `oxygen dashboards widgets kinds` to see each one with an example.")
162
+ .description("Add a widget. Kinds: chart, view, iframe, rich_text, code, live_source, deliverability_evidence. Run `oxygen dashboards widgets kinds` for each configuration shape and an example.")
163
163
  .argument("<dashboard>", "Dashboard slug or id.")
164
- .argument("<kind>", "chart | view | iframe | rich_text | code | live_source")
164
+ .argument("<kind>", "chart | view | iframe | rich_text | code | live_source | deliverability_evidence")
165
165
  .argument("<title>", "Widget title.")
166
166
  .option("--tab <tab_id>", "Tab to place it on. Defaults to the dashboard's first tab.")
167
167
  .option("--config <json>", "Widget configuration as JSON.")
@@ -143,6 +143,40 @@ export function registerDbFamily(program, handleAsyncAction) {
143
143
  },
144
144
  });
145
145
  });
146
+ }))
147
+ .addCommand(new Command("backfill-linkedin-member-ids")
148
+ .description("Fill the people LinkedIn Member ID identity from LinkedIn data one workspace already holds (member-id profile URLs, profile payloads, connections, enrollments). Staff only; 0 credits, no provider calls. Defaults to a dry run that reports filled, conflicts, ambiguous and mismatched counts.")
149
+ .option("--org <id>", "The organization id to backfill. Required.")
150
+ .option("--live", "Write the resolved ids, converging the People template first if needed. Requires --confirm.")
151
+ .option("--revert", "Archive exactly the identities a --live run wrote and clear their cells. Requires --confirm.")
152
+ .option("--confirm", "Confirm --live or --revert.")
153
+ .option("--limit <n>", "Maximum people rows to scan in this batch. Defaults to 2000; hard cap is 10000.")
154
+ .option("--cursor <row-id>", "Continue a scan from the previous run's nextCursor.")
155
+ .option("--json", "Print a JSON envelope.")
156
+ .action(async (options) => {
157
+ await handleAsyncAction("db backfill-linkedin-member-ids", options, () => {
158
+ if (options.live && options.revert) {
159
+ throw new OxygenError("invalid_request", "Pass --live or --revert, not both.", { exitCode: 1 });
160
+ }
161
+ const mode = options.live ? "live" : options.revert ? "revert" : "dry_run";
162
+ if (mode !== "dry_run" && !options.confirm) {
163
+ throw new OxygenError("confirmation_required", `Refusing to run --${mode} without --confirm.`, { exitCode: 1 });
164
+ }
165
+ const limit = readPositiveInt(options.limit);
166
+ // `--org` is also a root option; read both scopes (see backfill-upsert-indexes).
167
+ const organizationId = readOption(options.org) ?? readOption(program.opts().org);
168
+ const cursor = readOption(options.cursor);
169
+ return requestOxygen("/api/cli/db/backfill-linkedin-member-ids", {
170
+ method: "POST",
171
+ body: {
172
+ mode,
173
+ confirm: Boolean(options.confirm),
174
+ ...(organizationId ? { organization_id: organizationId } : {}),
175
+ ...(limit !== undefined ? { limit } : {}),
176
+ ...(cursor ? { cursor } : {}),
177
+ },
178
+ });
179
+ });
146
180
  }))
147
181
  .addCommand(new Command("cost-policy")
148
182
  .description("Read-only, 0 credits. Show the retired Neon cost-policy status. Its managed field describes the policy, not database hosting; PostgreSQL has no per-workspace sleep control.")
@@ -112,7 +112,7 @@ export function registerFindFamily(program, handleAsyncAction) {
112
112
  await handleAsyncAction("find phone", options, () => requestOxygen("/api/cli/find/run", { method: "POST", body: buildFindBody("phone", options) }));
113
113
  }))
114
114
  .addCommand(new Command("linkedin")
115
- .description("Resolve a person's LinkedIn URL from name + company (or email), with identity validation. Each lane tried bills until the 2026-09 repricing date; from it a URL found that matches the person costs a flat 5 credits and a miss costs 0.")
115
+ .description("Resolve a person's LinkedIn URL from name + company (or email), with identity validation. Each lane tried bills until the 2026-09 repricing date; from it a URL found that matches the person costs one flat price, which the dry run quotes, and a miss costs 0.")
116
116
  .option("--full-name <name>", "Person full name.")
117
117
  .option("--email <email>", "Known email.")
118
118
  .option("--company-domain <domain>", "Company apex domain.")
@@ -789,6 +789,7 @@ export function registerKnowledgeFamily(program, handleAsyncAction) {
789
789
  .option("--body <markdown>", "With --approved: save this body verbatim (the guide a preview showed, or your edit) with no new AI call, at 0 credits.")
790
790
  .option("--title <title>", "Optional title for a newly created page/asset.")
791
791
  .option("--limit <n>", "Voice only: how many recent sent messages to learn from (max 200).")
792
+ .option("--sender <profile_id>", "Voice only, Network Mining (dev): learn one sender profile's voice from their own hand-written DMs and posts; --approved saves it and proposes it as that sender's voice.")
792
793
  .option("--max-credits <n>", "Hard cap OXYGEN enforces on this call: it is charged exactly its estimate, so never more than n. A higher estimate is refused before any AI call, with nothing charged, and the refusal reports the estimate (--max-credits 0 shows it for free). --approved --body makes no AI call and needs none.")
793
794
  .option("--json", "Print a JSON envelope.")
794
795
  .action(async (options) => {
@@ -797,6 +798,7 @@ export function registerKnowledgeFamily(program, handleAsyncAction) {
797
798
  body: {
798
799
  kind: options.kind ?? "campaign_learnings",
799
800
  ...(readOption(options.sequence) ? { sequence: readOption(options.sequence) } : {}),
801
+ ...(readOption(options.sender) ? { sender: readOption(options.sender) } : {}),
800
802
  ...(options.approved ? { approved: true } : {}),
801
803
  ...(readOption(options.body) ? { body: readOption(options.body) } : {}),
802
804
  ...(readOption(options.title) ? { title: readOption(options.title) } : {}),
@@ -3,6 +3,7 @@ import { readNonNegativeInt, readPositiveInt } from "../cli-values.js";
3
3
  import { readCsvOption, readNonNegativeNumber, splitCommaList } from "../command-support.js";
4
4
  import { requestOxygen } from "../http-client.js";
5
5
  import { readOption } from "../util.js";
6
+ import { withDeliverabilityEvidenceCommands } from "../deliverability-evidence-commands.js";
6
7
  import { OxygenError } from "@oxygen/shared/cli-result";
7
8
  import { inferRowsFileFormat, parseXlsxWorkbookBuffer } from "@oxygen/shared/file-import";
8
9
  import { MAILBOX_IMPORT_FILE_MAX_BYTES as SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES, MAILBOX_IMPORT_ROW_LIMIT as SHARED_MAILBOX_IMPORT_ROW_LIMIT, normalizeMailboxImportFile as normalizeSharedMailboxImportFile, normalizeMailboxImportVendor as normalizeSharedMailboxImportVendor, normalizeMailboxWorkbookRows, parseMailboxImportText, summarizeMailboxImportValidation as summarizeSharedMailboxImportValidation } from "@oxygen/shared/mailbox-import";
@@ -880,7 +881,7 @@ export function registerMailboxesFamily(program, handleAsyncAction) {
880
881
  .action(async (options) => {
881
882
  await handleAsyncAction("mailboxes oauth-health", options, () => requestOxygen("/api/cli/mailboxes/oauth-health"));
882
883
  }))
883
- .addCommand(new Command("deliverability")
884
+ .addCommand(withDeliverabilityEvidenceCommands(new Command("deliverability")
884
885
  .description("Assess every mailbox origin for OXYGEN deliverability and connect credential-capable Google Workspace mailboxes from managed, Zapmail, or compatible encrypted external imports. Manual/native Google rows report credential_required; Microsoft 365/Azure reports vendor_blocked. Preview never materializes a password; approved execution fetches it just in time and never prints it.")
885
886
  .addCommand(new Command("connect")
886
887
  .description("Preview or connect selected mailboxes to OXYGEN deliverability. Managed mode is 100 credits/inbox-month, with two placement tests per inbox per cycle included; an extra managed test costs 66 credits. The preview shows the price in force. Own-key monitoring has no monthly Oxygen fee; placement tests pay the in-force platform fee and vendor charges separately. Google Workspace requires a real app password, fetched only after approval. Microsoft 365/Azure is explicitly vendor-blocked because OXYGEN deliverability exposes neither Microsoft OAuth nor tenant consent; Oxygen never downgrades it to password auth. Starts with a 0-credit, no-provider-write preview. Account connection sends no placement probe.")
@@ -981,7 +982,7 @@ export function registerMailboxesFamily(program, handleAsyncAction) {
981
982
  }
982
983
  return requestOxygen(path);
983
984
  });
984
- })))
985
+ })), handleAsyncAction))
985
986
  .addCommand(new Command("delegation")
986
987
  .description("Google sending-domain delegation only — not Microsoft OAuth and not a credential-file handoff to OXYGEN Warm-up or OXYGEN deliverability. Reports covered domains plus the exact client id/scopes; --probe mints a real token per mailbox. Read-only, sends no mail, 0 Oxygen credits.")
987
988
  .option("--domain <domain>", "Only report this sending domain.")
@@ -1,6 +1,7 @@
1
1
  import { requestOxygen } from "../http-client.js";
2
2
  import { readOption } from "../util.js";
3
- import { Command } from "commander";
3
+ import { Command, Option } from "commander";
4
+ import { networkMiningHostServed } from "../network-mining-host.js";
4
5
  export function registerMessagesFamily(program, handleAsyncAction) {
5
6
  program.addCommand(new Command("messages")
6
7
  .description("Cross-channel message corpus: every individual email + LinkedIn + WhatsApp message as one searchable stream (Postgres full-text search over bodies, keyset paginated by recency), plus INBOX-WIDE conversation analytics. NOT campaign performance: for per-channel campaign reply rates use `oxygen sequences analytics`. Message-level, unlike inbox (conversation-level triage).")
@@ -12,6 +13,8 @@ export function registerMessagesFamily(program, handleAsyncAction) {
12
13
  .option("--direction <direction>", "Filter by direction: inbound or outbound.")
13
14
  .option("--account <id>", "Mailbox id (email) or sender account id (DM) to filter to.")
14
15
  .option("--contact <text>", "Filter by counterpart name or handle (address / provider id).")
16
+ .addOption(new Option("--record <record>", "Network Mining (dev): one CRM record's messages, by row id or identity (email, LinkedIn URL); its linked conversations, never a name match.").hideHelp(!networkMiningHostServed()))
17
+ .addOption(new Option("--object <slug>", "With --record: the record's CRM object. Defaults to people.").hideHelp(!networkMiningHostServed()))
15
18
  .option("--sequence-id <id>", "Email only: campaign (sequence) id (restricts the result to email).")
16
19
  .option("--status <keys>", "Comma-separated conversation status keys (e.g. interested,meeting_booked).")
17
20
  .option("--tag <tag>", "Only messages whose conversation carries this workspace tag (see `oxygen tags list`).")
@@ -32,6 +35,8 @@ export function registerMessagesFamily(program, handleAsyncAction) {
32
35
  ["direction", "direction"],
33
36
  ["account", "account"],
34
37
  ["contact", "contact"],
38
+ ["record", "record"],
39
+ ["object", "object"],
35
40
  ["sequenceId", "sequence_id"],
36
41
  ["status", "status"],
37
42
  ["tag", "tag"],
@@ -295,6 +295,7 @@ export function registerSendersFamily(program, handleAsyncAction) {
295
295
  .option("--last-name <name>", "New last name — the exact one an inbox vendor stamps on mailboxes ordered for this sender. Fix it BEFORE ordering: the vendor cannot rename a mailbox afterwards.")
296
296
  .option("--avatar-url <url>", "New avatar image URL (empty string clears it). Must be a public https URL; an expiring link is stored but marked non-durable and is NOT sent to an inbox vendor.")
297
297
  .option("--status <status>", "New status: active, paused, or archived.")
298
+ .option("--voice-page <slug>", "Network Mining (dev): propose this voice page as the sender's own voice; a person approves it in Knowledge proposals. \"none\" unbinds (people only).")
298
299
  .option("--json", "Print a JSON envelope.")
299
300
  .action(async (id, options) => {
300
301
  await handleAsyncAction("senders profiles update", options, () => requestOxygen(`/api/cli/senders/profiles/${encodeURIComponent(id)}`, {
@@ -305,6 +306,7 @@ export function registerSendersFamily(program, handleAsyncAction) {
305
306
  ...(readOption(options.lastName) ? { last_name: readOption(options.lastName) } : {}),
306
307
  ...(options.avatarUrl !== undefined ? { avatar_url: readOption(options.avatarUrl) ?? "" } : {}),
307
308
  ...(readOption(options.status) ? { status: readOption(options.status) } : {}),
309
+ ...(readOption(options.voicePage) ? { voice_page: readOption(options.voicePage) } : {}),
308
310
  },
309
311
  }));
310
312
  }))
@@ -1961,7 +1961,7 @@ export function registerSequencesFamily(program, handleAsyncAction) {
1961
1961
  .description("Preview or start a sequence. Without --approved this is the launch check and changes nothing: it returns launch_readiness (blockers and warnings), sender_journey (each step's From, candidate inboxes and assignment; no leads needed), rendered pending-lead LinkedIn/WhatsApp copy, character counts/limits, copy blockers, per-kind scope, sender/mailbox capacity, CRM readiness and safety caps. Email copy is not rendered; inspect its saved templates and unsubscribe settings with sequences get. Live start requires --approved; --max-credits is optional. Email/WhatsApp/CRM-task journeys additionally require --max-live-sends because their external actions cost 0 Oxygen credits. --dry-run starts the sequence in simulated mode (it leaves draft), with no send, provider campaign, CRM task or credits. Live start enables the standing reply → CRM automation only if it has never been configured. Planned counts include only leads each step's channel can reach; skipped_actions_by_reason and channel_coverage show the rest.")
1962
1962
  .argument("<sequence>", "Sequence id or slug.")
1963
1963
  .option("--approved", "Approve and activate live. Without this flag, returns a preview only, including copy_preview rendered samples and blockers.")
1964
- .option("--max-credits <n>", "Optional credit ceiling. Ordinary sequences require >0; guided LinkedIn permits 0 with a positive action ceiling and exact launch review.")
1964
+ .option("--max-credits <n>", "Optional credit ceiling (0 or more); 0 approves no credit spend and is refused when AI-written comments bill per draft. Guided LinkedIn also needs a positive action ceiling and the exact launch review.")
1965
1965
  .option("--max-live-sends <n>", "Total live-send and CRM-write ceiling across channels, including LinkedIn invites/messages (positive integer). Required for email, WhatsApp, or crm_task steps; guided LinkedIn binds its positive cap in launch_review.")
1966
1966
  .option("--launch-review-json <json>", "Guided onboarding: echo the exact preview.launch_review (digest, expiry, positive action ceiling and nonnegative credit cap). Changed inputs require a new preview.")
1967
1967
  .option("--launch-review-file <path>", "Read the exact launch_review object from a JSON file instead of --launch-review-json.")
@@ -1,9 +1,10 @@
1
1
  import { parseJsonObject, readPositiveInt } from "../cli-values.js";
2
- import { CADENCE_FLAG_DESCRIPTION, buildCrmLeadsTodayPath, buildCrmSignalRecordBody, readCsvOption, readFileIfPresent, readPositiveNumber, readSearchPlanJson } from "../command-support.js";
2
+ import { CADENCE_FLAG_DESCRIPTION, buildCrmLeadsTodayPath, leadsTodayRankOption, buildCrmSignalRecordBody, readCsvOption, readFileIfPresent, readPositiveNumber, readSearchPlanJson } from "../command-support.js";
3
3
  import { requestOxygen } from "../http-client.js";
4
4
  import { readOption } from "../util.js";
5
5
  import { OxygenError } from "@oxygen/shared/cli-result";
6
- import { Command } from "commander";
6
+ import { Command, Option } from "commander";
7
+ import { networkMiningHostServed } from "../network-mining-host.js";
7
8
  function buildSignalsListPath(options) {
8
9
  const params = new URLSearchParams();
9
10
  if (options.types)
@@ -12,6 +13,11 @@ function buildSignalsListPath(options) {
12
13
  params.set("since", options.since);
13
14
  if (options.limit)
14
15
  params.set("limit", options.limit);
16
+ if (options.record) {
17
+ params.set("record", options.record);
18
+ if (options.object)
19
+ params.set("object", options.object);
20
+ }
15
21
  const query = params.toString();
16
22
  return query ? `/api/cli/signals?${query}` : "/api/cli/signals";
17
23
  }
@@ -148,6 +154,8 @@ export function registerSignalsFamily(program, handleAsyncAction) {
148
154
  .option("--types <types>", "Comma-separated signal types to include. Defaults to all four.")
149
155
  .option("--since <days>", "Look-back window in days. Defaults to 7; your plan's maximum (30 on free, up to 365) is in `oxygen limits show`.")
150
156
  .option("--limit <limit>", "Maximum events to return. Defaults to 25; your plan's maximum (100 on free, up to 1,000) is in `oxygen limits show`.")
157
+ .addOption(new Option("--record <record>", "Network Mining (dev): one CRM record's signals, by row id or identity (email, LinkedIn URL).").hideHelp(!networkMiningHostServed()))
158
+ .addOption(new Option("--object <slug>", "With --record: the record's CRM object. Defaults to people.").hideHelp(!networkMiningHostServed()))
151
159
  .option("--json", "Print a JSON envelope.")
152
160
  .action(async (options) => {
153
161
  await handleAsyncAction("signals list", options, () => requestOxygen(buildSignalsListPath(options)));
@@ -183,6 +191,7 @@ export function registerSignalsFamily(program, handleAsyncAction) {
183
191
  .description("List people to contact today, ranked by their most recent GTM intent signal. When the output has rankedBy, equal signals are ordered by the company's ICP fit (see each lead's fit). Do-not-contact filtering is best-effort (email-only leads surface unchecked).")
184
192
  .option("--limit <limit>", "Maximum leads to return. Defaults to 25, max 100.")
185
193
  .option("--within-days <days>", "Signal look-back window in days. Defaults to 7, max 30.")
194
+ .addOption(leadsTodayRankOption())
186
195
  .option("--json", "Print a JSON envelope.")
187
196
  .action(async (options) => {
188
197
  await handleAsyncAction("signals leads-today", options, () => requestOxygen(buildCrmLeadsTodayPath(options)));
@@ -0,0 +1,4 @@
1
+ import { Command } from "commander";
2
+ import type { ExtractedCommandActionHandler } from "./command-support.js";
3
+ /** Adds the evidence leaves to the `mailboxes deliverability` group and returns it. */
4
+ export declare function withDeliverabilityEvidenceCommands(root: Command, handle: ExtractedCommandActionHandler): Command;
@@ -0,0 +1,104 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+ import { DELIVERABILITY_CHECK_LIST_FIELDS, DELIVERABILITY_EVIDENCE_FIELDS, DELIVERABILITY_OBSERVATION_LIST_FIELDS, DELIVERABILITY_SURFACE_BUCKETS, DELIVERABILITY_SURFACE_CHECK_KINDS, DELIVERABILITY_SURFACE_FINDING_STATUSES, DELIVERABILITY_SURFACE_FOLDERS, DELIVERABILITY_SURFACE_MAX_LIMIT, DELIVERABILITY_SURFACE_RECEIVER_PROVIDERS, DELIVERABILITY_SURFACE_RUN_STATUSES, DELIVERABILITY_SURFACE_SOURCES, DELIVERABILITY_SURFACE_SUBJECT_TYPES, deliverabilitySurfaceQuery, } from "@oxygen/shared/deliverability-surface";
4
+ import { Command, Option } from "commander";
5
+ import { requestOxygen } from "./http-client.js";
6
+ const WINDOW_HELP = "ISO 8601 time or a relative window such as 7d or 24h.";
7
+ const LIMIT_HELP = `Rows per page (1–${DELIVERABILITY_SURFACE_MAX_LIMIT}, default 25).`;
8
+ /** Commander camelCases flags; the routes read snake_case query fields of the same name. */
9
+ function queryValues(fields, options) {
10
+ return Object.fromEntries(fields.map((field) => [field, options[field.replace(/_([a-z])/g, (_, letter) => letter.toUpperCase())]]));
11
+ }
12
+ function readTextOption(options, inline, file, label) {
13
+ const value = options[inline];
14
+ const path = options[file];
15
+ if (typeof value === "string" && typeof path === "string")
16
+ throw new Error(`Pass ${label} inline or from a file, not both.`);
17
+ if (typeof path === "string")
18
+ return readFileSync(resolve(path), "utf8");
19
+ return typeof value === "string" ? value : undefined;
20
+ }
21
+ /** Adds the evidence leaves to the `mailboxes deliverability` group and returns it. */
22
+ export function withDeliverabilityEvidenceCommands(root, handle) {
23
+ root.addCommand(new Command("checks")
24
+ .description("Stored deliverability check runs: DNS authentication, BIMI, tracking CNAME, blacklist, copy score and header proof, each with typed findings. 0 credits; runs no check.")
25
+ .addCommand(new Command("list")
26
+ .description("List stored check runs, newest first, with pass/warn/fail/unknown finding counts. Follow next_cursor with the same filters.")
27
+ .addOption(new Option("--kind <kind>", "Check kind.").choices([...DELIVERABILITY_SURFACE_CHECK_KINDS]))
28
+ .addOption(new Option("--subject-type <type>", "What was checked.").choices([...DELIVERABILITY_SURFACE_SUBJECT_TYPES]))
29
+ .option("--subject <subject>", "Exact subject: a domain, mailbox id or copy content hash.")
30
+ .addOption(new Option("--status <status>", "Run status.").choices([...DELIVERABILITY_SURFACE_RUN_STATUSES]))
31
+ .addOption(new Option("--finding-status <status>", "Only runs with at least one finding of this status.").choices([...DELIVERABILITY_SURFACE_FINDING_STATUSES]))
32
+ .option("--since <time>", `Started at or after. ${WINDOW_HELP}`)
33
+ .option("--until <time>", `Started before. ${WINDOW_HELP}`)
34
+ .option("--limit <n>", LIMIT_HELP)
35
+ .option("--cursor <id>", "next_cursor from the previous page.")
36
+ .option("--json", "Print a JSON envelope.")
37
+ .action(async (options) => handle("mailboxes deliverability checks list", options, () => requestOxygen(`/api/cli/deliverability/checks${deliverabilitySurfaceQuery(DELIVERABILITY_CHECK_LIST_FIELDS, queryValues(DELIVERABILITY_CHECK_LIST_FIELDS, options))}`))))
38
+ .addCommand(new Command("get")
39
+ .description("Read one stored check run: every finding (worst first) with its value, and the bounded raw evidence.")
40
+ .argument("<id>", "Check run id from `checks list`.")
41
+ .option("--json", "Print a JSON envelope.")
42
+ .action(async (id, options) => handle("mailboxes deliverability checks get", options, () => requestOxygen(`/api/cli/deliverability/checks/${encodeURIComponent(id)}`)))));
43
+ root.addCommand(new Command("observations")
44
+ .description("Stored inbox-placement observations: one row per probe from workspace copy tests, cross-workspace tests and placement tests, with folder, tab, receiver provider, header proof and copy hash. 0 credits.")
45
+ .addCommand(new Command("list")
46
+ .description("List placement observations, newest first by placement time (sent, else observed, else recorded), the time evidence buckets on. Follow next_cursor with the same filters.")
47
+ .addOption(new Option("--source <source>", "Test source.").choices([...DELIVERABILITY_SURFACE_SOURCES]))
48
+ .option("--sender-domain <domain>", "Sending domain.")
49
+ .option("--sender-mailbox-id <id>", "Sending mailbox id.")
50
+ .addOption(new Option("--receiver-provider <provider>", "Receiving mailbox provider.").choices([...DELIVERABILITY_SURFACE_RECEIVER_PROVIDERS]))
51
+ .addOption(new Option("--folder <folder>", "Observed folder.").choices([...DELIVERABILITY_SURFACE_FOLDERS]))
52
+ .option("--run-ref <id>", "One test: workspace test id or placement test id.")
53
+ .option("--content-hash <hash>", "Copy content hash (64 hex characters).")
54
+ .option("--since <time>", `Placed at or after. ${WINDOW_HELP}`)
55
+ .option("--until <time>", `Placed before. ${WINDOW_HELP}`)
56
+ .option("--limit <n>", LIMIT_HELP)
57
+ .option("--cursor <id>", "next_cursor from the previous page.")
58
+ .option("--json", "Print a JSON envelope.")
59
+ .action(async (options) => handle("mailboxes deliverability observations list", options, () => requestOxygen(`/api/cli/deliverability/observations${deliverabilitySurfaceQuery(DELIVERABILITY_OBSERVATION_LIST_FIELDS, queryValues(DELIVERABILITY_OBSERVATION_LIST_FIELDS, options))}`)))));
60
+ root.addCommand(new Command("evidence")
61
+ .description("What changed and what placement did, for one sender domain, sender mailbox, copy hash or receiver provider: placement per UTC day or week and per receiver provider, latest DNS/BIMI/blacklist state with every finding change, copy scores, campaign outcomes and placement Signals. Rates carry sample sizes. Defaults to the last 28 days by day. 0 credits.")
62
+ .option("--sender-domain <domain>", "Sending domain.")
63
+ .option("--sender-mailbox-id <id>", "Sending mailbox id.")
64
+ .option("--content-hash <hash>", "Copy content hash.")
65
+ .addOption(new Option("--receiver-provider <provider>", "Receiving mailbox provider.").choices([...DELIVERABILITY_SURFACE_RECEIVER_PROVIDERS]))
66
+ .option("--since <time>", `Window start (default 28 days before until). ${WINDOW_HELP}`)
67
+ .option("--until <time>", `Window end (default now). ${WINDOW_HELP}`)
68
+ .addOption(new Option("--bucket <bucket>", "Bucket size (day: at most 120 UTC day buckets, so --since 119d; week: 60).").choices([...DELIVERABILITY_SURFACE_BUCKETS]))
69
+ .option("--json", "Print a JSON envelope.")
70
+ .action(async (options) => handle("mailboxes deliverability evidence", options, () => requestOxygen(`/api/cli/deliverability/evidence${deliverabilitySurfaceQuery(DELIVERABILITY_EVIDENCE_FIELDS, queryValues(DELIVERABILITY_EVIDENCE_FIELDS, options))}`))));
71
+ root.addCommand(new Command("compare")
72
+ .description("Diff two stored runs of one kind: two placement tests (workspace test or placement test ids) by receiver provider with sample sizes, copy and rule changes and domain check states at each run; or two check runs of one kind (check run ids) finding by finding. 0 credits.")
73
+ .argument("<a>", "First run: check run id, workspace test id or placement test id.")
74
+ .argument("<b>", "Second run of the same kind.")
75
+ .option("--json", "Print a JSON envelope.")
76
+ .action(async (a, b, options) => handle("mailboxes deliverability compare", options, () => requestOxygen(`/api/cli/deliverability/compare${deliverabilitySurfaceQuery(["a", "b"], { a, b })}`))));
77
+ root.addCommand(new Command("score-copy")
78
+ .description("Score one email copy for spam-filter content signals on OXYGEN's own scorer and store it as a copy-score check run (content hash, score, fired rules, link domains). Sends nothing; 0 credits.")
79
+ .requiredOption("--subject <text>", "Subject line.")
80
+ .option("--body <text>", "Plain-text body.")
81
+ .option("--body-file <path>", "Read the plain-text body from a file.")
82
+ .option("--html <html>", "HTML body.")
83
+ .option("--html-file <path>", "Read the HTML body from a file.")
84
+ .option("--from <address>", "Sender address; its domain is scored with the copy (default sender@example.com).")
85
+ .option("--request-id <id>", "Retry key: the same id returns the stored score of unchanged copy instead of scoring again (at most 5 attempts per id).")
86
+ .option("--json", "Print a JSON envelope.")
87
+ .action(async (options) => handle("mailboxes deliverability score-copy", options, () => {
88
+ const bodyText = readTextOption(options, "body", "bodyFile", "the body");
89
+ if (bodyText === undefined)
90
+ throw new Error("--body <text> or --body-file <path> is required.");
91
+ const bodyHtml = readTextOption(options, "html", "htmlFile", "the HTML body");
92
+ return requestOxygen("/api/cli/deliverability/score-copy", {
93
+ method: "POST",
94
+ body: {
95
+ subject: options.subject,
96
+ body_text: bodyText,
97
+ ...(bodyHtml !== undefined ? { body_html: bodyHtml } : {}),
98
+ ...(typeof options.from === "string" ? { from: options.from } : {}),
99
+ ...(typeof options.requestId === "string" ? { request_id: options.requestId } : {}),
100
+ },
101
+ });
102
+ })));
103
+ return root;
104
+ }
@@ -0,0 +1,21 @@
1
+ import { type DiagnosticRecord } from "@oxygen/shared/diagnostics";
2
+ import type { LoadedCredentials } from "./credentials.js";
3
+ /** One invocation object shared by HTTP capture and diagnostic orchestration. */
4
+ export type CliDiagnosticContext = {
5
+ record: DiagnosticRecord;
6
+ scope: Promise<LoadedCredentials | null>;
7
+ credentialEnv: NodeJS.ProcessEnv;
8
+ requests: unknown[];
9
+ bytes: number;
10
+ droppedBytes: number;
11
+ stdout: string[];
12
+ stderr: string[];
13
+ restore: () => void;
14
+ organizationRef: string | null;
15
+ signalHandlers: (() => void)[];
16
+ };
17
+ export declare function currentCliDiagnosticContext(): CliDiagnosticContext | undefined;
18
+ export declare function setCliDiagnosticContext(context: CliDiagnosticContext | undefined): void;
19
+ export declare function cliDiagnosticInvocation(): string | undefined;
20
+ export declare function cliDiagnosticHeaders(): Record<string, string>;
21
+ export declare function recordCliDiagnosticRequest(input: unknown): void;
@@ -0,0 +1,25 @@
1
+ import { DIAGNOSTIC_INVOCATION_HEADER, DIAGNOSTIC_RECORD_MAX_BYTES, sanitizeDiagnosticContent } from "@oxygen/shared/diagnostics";
2
+ let active;
3
+ export function currentCliDiagnosticContext() { return active; }
4
+ export function setCliDiagnosticContext(context) { active = context; }
5
+ export function cliDiagnosticInvocation() { return active?.record.invocation_id; }
6
+ export function cliDiagnosticHeaders() {
7
+ return active ? { [DIAGNOSTIC_INVOCATION_HEADER]: active.record.invocation_id, traceparent: active.record.traceparent } : {};
8
+ }
9
+ export function recordCliDiagnosticRequest(input) {
10
+ if (!active)
11
+ return;
12
+ try {
13
+ const sanitized = sanitizeDiagnosticContent(input);
14
+ const size = Buffer.byteLength(JSON.stringify(sanitized));
15
+ if (active.bytes + size <= DIAGNOSTIC_RECORD_MAX_BYTES) {
16
+ active.requests.push(sanitized);
17
+ active.bytes += size;
18
+ }
19
+ else
20
+ active.droppedBytes += size;
21
+ }
22
+ catch {
23
+ active.droppedBytes++;
24
+ }
25
+ }
@@ -1,5 +1,6 @@
1
1
  import { type DiagnosticRecord } from "@oxygen/shared/diagnostics";
2
2
  import { type StoredCredentials } from "./credentials.js";
3
+ export { cliDiagnosticHeaders, cliDiagnosticInvocation, recordCliDiagnosticRequest } from "./diagnostic-context.js";
3
4
  export type CliDiagnosticScope = {
4
5
  api_url: string;
5
6
  fingerprint: string | null;
@@ -14,9 +15,6 @@ export declare function cliDiagnosticDirectory(): string;
14
15
  export declare function credentialFingerprint(credentials: StoredCredentials): string;
15
16
  export declare function beginCliDiagnostics(argv?: string[]): void;
16
17
  export declare function setCliDiagnosticOperation(command: string): void;
17
- export declare function cliDiagnosticInvocation(): string | undefined;
18
- export declare function cliDiagnosticHeaders(): Record<string, string>;
19
- export declare function recordCliDiagnosticRequest(input: unknown): void;
20
18
  export declare function recordCliDiagnosticFailure(error: unknown): void;
21
19
  export declare function finishCliDiagnostics(exitCode: number): Promise<string | null>;
22
20
  export declare function writeCliDiagnosticSpool(directory: string, entry: CliDiagnosticSpoolEntry): Promise<string | null>;