@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
@@ -7,10 +7,11 @@ import { fileURLToPath } from "node:url";
7
7
  import { acquireDiagnosticLock } from "@oxygen/shared/diagnostic-lock";
8
8
  import { StringDecoder } from "node:string_decoder";
9
9
  import { gzipSync } from "node:zlib";
10
- import { createDiagnosticRecord, credentialSafeDiagnostic, diagnosticId, DIAGNOSTIC_INVOCATION_HEADER, DIAGNOSTIC_MAX_BYTES, DIAGNOSTIC_RECORD_MAX_BYTES, diagnosticOutcome, sanitizeDiagnosticArgv, sanitizeDiagnosticContent } from "@oxygen/shared/diagnostics";
10
+ import { createDiagnosticRecord, credentialSafeDiagnostic, diagnosticId, DIAGNOSTIC_MAX_BYTES, DIAGNOSTIC_RECORD_MAX_BYTES, diagnosticOutcome, sanitizeDiagnosticArgv, sanitizeDiagnosticContent } from "@oxygen/shared/diagnostics";
11
11
  import { OXYGEN_VERSION } from "@oxygen/shared/version";
12
12
  import { loadCredentialsWithEndpoint } from "./credentials.js";
13
- let active;
13
+ import { currentCliDiagnosticContext, recordCliDiagnosticRequest, setCliDiagnosticContext } from "./diagnostic-context.js";
14
+ export { cliDiagnosticHeaders, cliDiagnosticInvocation, recordCliDiagnosticRequest } from "./diagnostic-context.js";
14
15
  export function cliDiagnosticDirectory() {
15
16
  return join(process.env.OXYGEN_CONFIG_DIR?.trim() || (process.platform === "win32"
16
17
  ? join(process.env.APPDATA || join(homedir(), "AppData", "Roaming"), "oxygen")
@@ -20,7 +21,7 @@ export function credentialFingerprint(credentials) {
20
21
  return createHash("sha256").update(credentials.token).digest("hex");
21
22
  }
22
23
  export function beginCliDiagnostics(argv = process.argv.slice(2)) {
23
- if (active || process.env.OXYGEN_DIAGNOSTICS_ENABLED !== "1")
24
+ if (currentCliDiagnosticContext() || process.env.OXYGEN_DIAGNOSTICS_ENABLED !== "1")
24
25
  return;
25
26
  const invocation = createDiagnosticRecord({ surface: "cli", operation: "cli parse",
26
27
  client_version: OXYGEN_VERSION, input: { argv: sanitizeDiagnosticArgv(argv) },
@@ -32,7 +33,7 @@ export function beginCliDiagnostics(argv = process.argv.slice(2)) {
32
33
  const context = { record: invocation, credentialEnv, scope: loadCredentialsWithEndpoint(credentialEnv).catch(() => null),
33
34
  organizationRef: credentialEnv.OXYGEN_ORG?.trim() || null, signalHandlers: [],
34
35
  requests: [], bytes: 0, droppedBytes: 0, stdout: [], stderr: [], restore: () => undefined };
35
- active = context;
36
+ setCliDiagnosticContext(context);
36
37
  const restorers = [];
37
38
  for (const [stream, output] of [[process.stdout, context.stdout], [process.stderr, context.stderr]]) {
38
39
  const original = stream.write;
@@ -68,46 +69,27 @@ export function beginCliDiagnostics(argv = process.argv.slice(2)) {
68
69
  }
69
70
  }
70
71
  export function setCliDiagnosticOperation(command) {
72
+ const active = currentCliDiagnosticContext();
71
73
  if (active)
72
74
  active.record.operation = command.slice(0, 512);
73
75
  }
74
- export function cliDiagnosticInvocation() { return active?.record.invocation_id; }
75
- export function cliDiagnosticHeaders() {
76
- return active ? { [DIAGNOSTIC_INVOCATION_HEADER]: active.record.invocation_id, traceparent: active.record.traceparent } : {};
77
- }
78
- export function recordCliDiagnosticRequest(input) {
79
- if (!active)
80
- return;
81
- try {
82
- const sanitized = sanitizeDiagnosticContent(input);
83
- const size = Buffer.byteLength(JSON.stringify(sanitized));
84
- if (active.bytes + size <= DIAGNOSTIC_RECORD_MAX_BYTES) {
85
- active.requests.push(sanitized);
86
- active.bytes += size;
87
- }
88
- else
89
- active.droppedBytes += size;
90
- }
91
- catch {
92
- active.droppedBytes++;
93
- }
94
- }
95
76
  export function recordCliDiagnosticFailure(error) {
96
- if (!active)
77
+ if (!currentCliDiagnosticContext())
97
78
  return;
79
+ // An error getter can finish or replace the invocation before these writes.
98
80
  const code = error && typeof error === "object" && "code" in error ? String(error.code) : "unexpected_error";
99
- active.record.error_code = code;
100
- active.record.outcome = diagnosticOutcome(false, null, code);
81
+ currentCliDiagnosticContext().record.error_code = code;
82
+ currentCliDiagnosticContext().record.outcome = diagnosticOutcome(false, null, code);
101
83
  recordCliDiagnosticRequest({ error });
102
84
  }
103
85
  export async function finishCliDiagnostics(exitCode) {
104
- const context = active;
86
+ const context = currentCliDiagnosticContext();
105
87
  if (!context)
106
88
  return null;
107
89
  context.restore();
108
90
  for (const remove of context.signalHandlers)
109
91
  remove();
110
- active = undefined;
92
+ setCliDiagnosticContext(undefined);
111
93
  try {
112
94
  const loaded = await context.scope;
113
95
  const credentials = loaded?.credentials;
@@ -1,7 +1,7 @@
1
1
  import { readGeoExportPagination } from "@oxygen/shared/geo";
2
2
  import { OXYGEN_VERSION, OxygenError, isCliResult, isVersionGreater, parseSemver, readEnvelopeCompatibility, vercelProtectionBypassHeaders, withRetryAfterDetails, } from "@oxygen/shared";
3
3
  import { randomUUID } from "node:crypto";
4
- import { cliDiagnosticHeaders, cliDiagnosticInvocation, recordCliDiagnosticRequest } from "./diagnostics.js";
4
+ import { cliDiagnosticHeaders, cliDiagnosticInvocation, recordCliDiagnosticRequest } from "./diagnostic-context.js";
5
5
  import { readBoundedBinaryResponse, streamBinaryResponse } from "./refactor-internals/binary-download.js";
6
6
  import { recordMutatingRequestAnswered, recoverFromCliFloorRejection } from "./auto-update.js";
7
7
  import { defaultApiUrl, loadCredentialsWithEndpoint } from "./credentials.js";
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Network Mining is DEV-only and its server answers feature_disabled elsewhere.
3
+ * The CLI cannot read the server's gate, so it shows Network Mining commands
4
+ * (and routes discovery to them) only when its API host is not production.
5
+ */
6
+ export declare function networkMiningHostServed(env?: NodeJS.ProcessEnv): boolean;
@@ -0,0 +1,10 @@
1
+ import { defaultApiUrl } from "./credentials.js";
2
+ import { isProdApiUrl } from "./runtime.js";
3
+ /**
4
+ * Network Mining is DEV-only and its server answers feature_disabled elsewhere.
5
+ * The CLI cannot read the server's gate, so it shows Network Mining commands
6
+ * (and routes discovery to them) only when its API host is not production.
7
+ */
8
+ export function networkMiningHostServed(env = process.env) {
9
+ return !isProdApiUrl(defaultApiUrl(env));
10
+ }
@@ -5,7 +5,7 @@
5
5
  * "does foo@Bar.com match FOO@bar.com" answers the same everywhere — including
6
6
  * in the browser. Pure string functions — no I/O, no tenant context.
7
7
  */
8
- export declare const VALUE_NORMALIZATIONS: readonly ["exact_text_v1", "lower_trim_v1", "email_v1", "domain_v1", "linkedin_url_v1", "uuid_v1"];
8
+ export declare const VALUE_NORMALIZATIONS: readonly ["exact_text_v1", "lower_trim_v1", "email_v1", "domain_v1", "linkedin_url_v1", "linkedin_member_id_v1", "uuid_v1"];
9
9
  export type ValueNormalization = (typeof VALUE_NORMALIZATIONS)[number];
10
10
  export declare function isValueNormalization(value: unknown): value is ValueNormalization;
11
11
  /** `email_v1`: case-insensitive mailbox match. */
@@ -5,12 +5,14 @@
5
5
  * "does foo@Bar.com match FOO@bar.com" answers the same everywhere — including
6
6
  * in the browser. Pure string functions — no I/O, no tenant context.
7
7
  */
8
+ import { normalizeLinkedinMemberId } from "@oxygen/shared/linkedin-connection-identity";
8
9
  export const VALUE_NORMALIZATIONS = [
9
10
  "exact_text_v1",
10
11
  "lower_trim_v1",
11
12
  "email_v1",
12
13
  "domain_v1",
13
14
  "linkedin_url_v1",
15
+ "linkedin_member_id_v1",
14
16
  "uuid_v1",
15
17
  ];
16
18
  export function isValueNormalization(value) {
@@ -69,6 +71,9 @@ export function normalizeValue(normalization, value) {
69
71
  return normalizeDomain(value);
70
72
  case "linkedin_url_v1":
71
73
  return normalizeLinkedinUrl(value);
74
+ case "linkedin_member_id_v1":
75
+ // Case-preserving and provider-scoped: see normalizeLinkedinMemberId.
76
+ return normalizeLinkedinMemberId(value);
72
77
  case "uuid_v1":
73
78
  return normalizeUuid(value);
74
79
  case "lower_trim_v1":
@@ -133,4 +133,173 @@ export declare const AGENT_SESSION_TEMPLATES: readonly [{
133
133
  readonly actions: readonly ["oxygen_crm_notes_add"];
134
134
  readonly events: readonly [];
135
135
  }];
136
+ export declare function agentSessionTemplates(env?: Record<string, string | undefined>): ({
137
+ readonly id: "campaign-monitor";
138
+ readonly title: "Campaign monitor";
139
+ readonly goal: "Watch this native campaign. Compare sends, replies and meetings with earlier reviews; explain changes, propose improvements and schedule the next review.";
140
+ readonly resources: readonly ["sequences"];
141
+ readonly actions: readonly ["oxygen_sequences_set_status"];
142
+ readonly events: readonly ["sequencer.contact_activity.any"];
143
+ } | {
144
+ readonly id: "lead-replenishment";
145
+ readonly title: "Lead replenishment";
146
+ readonly goal: "Maintain a qualified lead pool in the selected Table for this native campaign. Research gaps, deduplicate and enroll only qualified leads within the approved scope.";
147
+ readonly resources: readonly ["tables", "sequences"];
148
+ readonly actions: readonly ["oxygen_rows_upsert", "oxygen_sequences_enroll"];
149
+ readonly events: readonly [];
150
+ } | {
151
+ readonly id: "reply-handling";
152
+ readonly title: "Reply handling";
153
+ readonly goal: "Review the selected conversations, remember prior responses, qualify intent and draft replies. Send only when explicitly authorized; hand off uncertain or sensitive replies.";
154
+ readonly resources: readonly ["conversations"];
155
+ readonly actions: readonly ["oxygen_inbox_set_status"];
156
+ readonly events: readonly ["crm.email_reply_classified"];
157
+ } | {
158
+ readonly id: "deliverability";
159
+ readonly title: "Deliverability";
160
+ readonly goal: "Watch the selected mailboxes for sending health changes. Explain evidence and recommend or apply only approved cap and status adjustments.";
161
+ readonly resources: readonly ["mailboxes"];
162
+ readonly actions: readonly ["oxygen_mailboxes_set_cap", "oxygen_mailboxes_set_status"];
163
+ readonly events: readonly [];
164
+ } | {
165
+ readonly id: "account-research";
166
+ readonly title: "Account research";
167
+ readonly goal: "Maintain cited account research for the selected records and knowledge pages. Track changes and recommend next actions; keep facts separate from hypotheses.";
168
+ readonly resources: readonly ["records", "knowledge"];
169
+ readonly actions: readonly ["oxygen_crm_notes_add", "oxygen_knowledge_page_upsert"];
170
+ readonly events: readonly [];
171
+ } | {
172
+ readonly id: "inbound-qualification";
173
+ readonly title: "Inbound qualification";
174
+ readonly goal: "Qualify the selected inbound conversations and records against workspace ICP and product knowledge. Record evidence and recommendations; hand off high intent to a person.";
175
+ readonly resources: readonly ["conversations", "records"];
176
+ readonly actions: readonly ["oxygen_inbox_set_status", "oxygen_crm_notes_add"];
177
+ readonly events: readonly ["crm.email_reply_classified"];
178
+ } | {
179
+ readonly id: "crm-hygiene";
180
+ readonly title: "CRM hygiene";
181
+ readonly goal: "Review the selected CRM objects for duplicates and missing or inconsistent fields. Propose repairs with evidence, and apply only explicitly authorized changes.";
182
+ readonly resources: readonly ["objects"];
183
+ readonly actions: readonly ["oxygen_crm_merge"];
184
+ readonly events: readonly [];
185
+ } | {
186
+ readonly id: "pipeline-review";
187
+ readonly title: "Pipeline review";
188
+ readonly goal: "Review the selected pipeline and lifecycle stages. Remember earlier reviews, identify stalled opportunities and recommend the next action with evidence.";
189
+ readonly resources: readonly ["objects", "records"];
190
+ readonly actions: readonly ["oxygen_crm_notes_add"];
191
+ readonly events: readonly [];
192
+ } | {
193
+ id: "campaign-monitor";
194
+ title: "Campaign monitor";
195
+ resources: readonly ["sequences"];
196
+ actions: readonly ["oxygen_sequences_set_status"];
197
+ events: readonly ["sequencer.contact_activity.any"];
198
+ goal: string;
199
+ } | {
200
+ id: "lead-replenishment";
201
+ title: "Lead replenishment";
202
+ resources: readonly ["tables", "sequences"];
203
+ actions: readonly ["oxygen_rows_upsert", "oxygen_sequences_enroll"];
204
+ events: readonly [];
205
+ goal: string;
206
+ } | {
207
+ id: "reply-handling";
208
+ title: "Reply handling";
209
+ resources: readonly ["conversations"];
210
+ actions: readonly ["oxygen_inbox_set_status"];
211
+ events: readonly ["crm.email_reply_classified"];
212
+ goal: string;
213
+ } | {
214
+ id: "deliverability";
215
+ title: "Deliverability";
216
+ resources: readonly ["mailboxes"];
217
+ actions: readonly ["oxygen_mailboxes_set_cap", "oxygen_mailboxes_set_status"];
218
+ events: readonly [];
219
+ goal: string;
220
+ } | {
221
+ id: "account-research";
222
+ title: "Account research";
223
+ resources: readonly ["records", "knowledge"];
224
+ actions: readonly ["oxygen_crm_notes_add", "oxygen_knowledge_page_upsert"];
225
+ events: readonly [];
226
+ goal: string;
227
+ } | {
228
+ id: "inbound-qualification";
229
+ title: "Inbound qualification";
230
+ resources: readonly ["conversations", "records"];
231
+ actions: readonly ["oxygen_inbox_set_status", "oxygen_crm_notes_add"];
232
+ events: readonly ["crm.email_reply_classified"];
233
+ goal: string;
234
+ } | {
235
+ id: "crm-hygiene";
236
+ title: "CRM hygiene";
237
+ resources: readonly ["objects"];
238
+ actions: readonly ["oxygen_crm_merge"];
239
+ events: readonly [];
240
+ goal: string;
241
+ } | {
242
+ id: "pipeline-review";
243
+ title: "Pipeline review";
244
+ resources: readonly ["objects", "records"];
245
+ actions: readonly ["oxygen_crm_notes_add"];
246
+ events: readonly [];
247
+ goal: string;
248
+ })[] | readonly [{
249
+ readonly id: "campaign-monitor";
250
+ readonly title: "Campaign monitor";
251
+ readonly goal: "Watch this native campaign. Compare sends, replies and meetings with earlier reviews; explain changes, propose improvements and schedule the next review.";
252
+ readonly resources: readonly ["sequences"];
253
+ readonly actions: readonly ["oxygen_sequences_set_status"];
254
+ readonly events: readonly ["sequencer.contact_activity.any"];
255
+ }, {
256
+ readonly id: "lead-replenishment";
257
+ readonly title: "Lead replenishment";
258
+ readonly goal: "Maintain a qualified lead pool in the selected Table for this native campaign. Research gaps, deduplicate and enroll only qualified leads within the approved scope.";
259
+ readonly resources: readonly ["tables", "sequences"];
260
+ readonly actions: readonly ["oxygen_rows_upsert", "oxygen_sequences_enroll"];
261
+ readonly events: readonly [];
262
+ }, {
263
+ readonly id: "reply-handling";
264
+ readonly title: "Reply handling";
265
+ readonly goal: "Review the selected conversations, remember prior responses, qualify intent and draft replies. Send only when explicitly authorized; hand off uncertain or sensitive replies.";
266
+ readonly resources: readonly ["conversations"];
267
+ readonly actions: readonly ["oxygen_inbox_set_status"];
268
+ readonly events: readonly ["crm.email_reply_classified"];
269
+ }, {
270
+ readonly id: "deliverability";
271
+ readonly title: "Deliverability";
272
+ readonly goal: "Watch the selected mailboxes for sending health changes. Explain evidence and recommend or apply only approved cap and status adjustments.";
273
+ readonly resources: readonly ["mailboxes"];
274
+ readonly actions: readonly ["oxygen_mailboxes_set_cap", "oxygen_mailboxes_set_status"];
275
+ readonly events: readonly [];
276
+ }, {
277
+ readonly id: "account-research";
278
+ readonly title: "Account research";
279
+ readonly goal: "Maintain cited account research for the selected records and knowledge pages. Track changes and recommend next actions; keep facts separate from hypotheses.";
280
+ readonly resources: readonly ["records", "knowledge"];
281
+ readonly actions: readonly ["oxygen_crm_notes_add", "oxygen_knowledge_page_upsert"];
282
+ readonly events: readonly [];
283
+ }, {
284
+ readonly id: "inbound-qualification";
285
+ readonly title: "Inbound qualification";
286
+ readonly goal: "Qualify the selected inbound conversations and records against workspace ICP and product knowledge. Record evidence and recommendations; hand off high intent to a person.";
287
+ readonly resources: readonly ["conversations", "records"];
288
+ readonly actions: readonly ["oxygen_inbox_set_status", "oxygen_crm_notes_add"];
289
+ readonly events: readonly ["crm.email_reply_classified"];
290
+ }, {
291
+ readonly id: "crm-hygiene";
292
+ readonly title: "CRM hygiene";
293
+ readonly goal: "Review the selected CRM objects for duplicates and missing or inconsistent fields. Propose repairs with evidence, and apply only explicitly authorized changes.";
294
+ readonly resources: readonly ["objects"];
295
+ readonly actions: readonly ["oxygen_crm_merge"];
296
+ readonly events: readonly [];
297
+ }, {
298
+ readonly id: "pipeline-review";
299
+ readonly title: "Pipeline review";
300
+ readonly goal: "Review the selected pipeline and lifecycle stages. Remember earlier reviews, identify stalled opportunities and recommend the next action with evidence.";
301
+ readonly resources: readonly ["objects", "records"];
302
+ readonly actions: readonly ["oxygen_crm_notes_add"];
303
+ readonly events: readonly [];
304
+ }];
136
305
  export declare function validateAgentSessionConfig(config: AgentSessionConfig, requireResources?: boolean): void;
@@ -1,5 +1,6 @@
1
1
  import { agentsEnabled, agentsEnabledIn } from "./agents-enabled.js";
2
2
  import { guidedActivationEnabled } from "./guided-activation.js";
3
+ import { networkMiningEnabled } from "./network-mining.js";
3
4
  import { validateContract } from "./contracts/schema.js";
4
5
  import { GUIDED_ACTIVATION_SCAN_POLICY_CONTRACT } from "./contracts/guided-activation.js";
5
6
  import { isStaffEmail } from "./staff.js";
@@ -50,6 +51,9 @@ export const AGENT_SESSION_ADAPTERS = {
50
51
  oxygen_rows_upsert: { resource: "tables", field: "table", read: false },
51
52
  oxygen_rows_update: { resource: "tables", field: "table", read: false },
52
53
  oxygen_tables_dedupe: { resource: "tables", field: "table", read: false },
54
+ oxygen_inbox_reply_oxygent_queue: { resource: "knowledge", field: "voice_slug", read: true },
55
+ oxygen_inbox_reply_oxygent_prepare: { resource: "knowledge", field: "voice_slug", read: false },
56
+ oxygen_inbox_reply_oxygent_voice: { resource: "knowledge", field: "voice_slug", read: false },
53
57
  oxygen_inbox_get: { resource: "conversations", field: "conversation", read: true },
54
58
  oxygen_inbox_set_status: { resource: "conversations", field: "conversation", read: false },
55
59
  oxygen_inbox_send: { resource: "conversations", field: "conversation", read: false },
@@ -127,6 +131,19 @@ export const AGENT_SESSION_TEMPLATES = [
127
131
  { id: "crm-hygiene", title: "CRM hygiene", goal: "Review the selected CRM objects for duplicates and missing or inconsistent fields. Propose repairs with evidence, and apply only explicitly authorized changes.", resources: ["objects"], actions: ["oxygen_crm_merge"], events: [] },
128
132
  { id: "pipeline-review", title: "Pipeline review", goal: "Review the selected pipeline and lifecycle stages. Remember earlier reviews, identify stalled opportunities and recommend the next action with evidence.", resources: ["objects", "records"], actions: ["oxygen_crm_notes_add"], events: [] },
129
133
  ];
134
+ /**
135
+ * Where Network Mining is served (DEV), record work starts from the one read
136
+ * of everything stored about a record. Elsewhere the templates are unchanged.
137
+ */
138
+ const BRIEF_FIRST_TEMPLATES = new Set(["account-research", "inbound-qualification"]);
139
+ const BRIEF_FIRST_SENTENCE = "Start each record with its brief (oxygen_crm_record_brief): one free read of its fields, signals, conversations, team connections, notes, tasks and company.";
140
+ export function agentSessionTemplates(env = typeof process === "undefined" ? {} : process.env) {
141
+ if (!networkMiningEnabled(env))
142
+ return AGENT_SESSION_TEMPLATES;
143
+ return AGENT_SESSION_TEMPLATES.map((template) => BRIEF_FIRST_TEMPLATES.has(template.id)
144
+ ? { ...template, goal: `${BRIEF_FIRST_SENTENCE} ${template.goal}` }
145
+ : template);
146
+ }
130
147
  export function validateAgentSessionConfig(config, requireResources = true) {
131
148
  const fail = (message) => { throw new OxygenError("invalid_session_config", message); };
132
149
  if (!config.title?.trim() || !config.goal?.trim())
@@ -107,6 +107,14 @@ export declare function serializeCapabilityRoute(route: CapabilityRouteMatch | n
107
107
  }, options?: {
108
108
  visibleSkills?: readonly string[];
109
109
  }): Record<string, unknown> | null;
110
+ /**
111
+ * Put the Network Mining record brief first on a record-dossier route where the
112
+ * host serves it, and take it off where it does not. The shared router decides
113
+ * by `networkMiningEnabled` in its own process; the CLI decides again by its
114
+ * API host, because its local environment says nothing about the server's.
115
+ * Idempotent; any other route is returned unchanged.
116
+ */
117
+ export declare function withRecordBrief<T extends Pick<CapabilityRouteMatch, "card" | "recommendedTools" | "recommendedCommands">>(route: T, served: boolean): T;
110
118
  /** One schedule for every channel of a Sequence, the same as Launch's Sending schedule. */
111
119
  export declare const SEQUENCE_SENDING_SCHEDULE_COMMAND = "oxygen sequences update <sequence> --schedule-timezone <IANA zone> --schedule-days mon-fri --schedule-hours-start 09:00 --schedule-hours-end 17:00 --json";
112
120
  /** Launch's unsubscribe switch: the visible link and the one-click headers together. */
@@ -1,4 +1,5 @@
1
1
  import { findEnrichmentIntentByAlias } from "./enrichment-intents.js";
2
+ import { networkMiningEnabled } from "./network-mining.js";
2
3
  import { RECIPE_PRIMITIVES } from "./recipes.js";
3
4
  // One compact semantic index covers every top-level /api/cli endpoint section.
4
5
  // It is navigation, never execution: live schemas, prices, readiness, and
@@ -567,6 +568,10 @@ const NORMALIZED_ROUTE_PRIMITIVES = OXYGEN_PRIMITIVE_ROUTES.map((card) => card.p
567
568
  if (NORMALIZED_PRIMITIVES.join("|") !== NORMALIZED_ROUTE_PRIMITIVES.join("|")) {
568
569
  throw new Error("OXYGEN capability routes must cover the shared fifteen-primitive catalog exactly.");
569
570
  }
571
+ /** Server env where one exists; a browser bundle has no process and serves no gated tool. */
572
+ function runtimeEnv() {
573
+ return typeof process === "undefined" ? {} : process.env;
574
+ }
570
575
  export function getCapabilityRoute(id) {
571
576
  return ROUTE_BY_ID.get(id.trim().toLowerCase()) ?? null;
572
577
  }
@@ -618,7 +623,7 @@ export function inferCapabilityRoute(query) {
618
623
  // Sourcing filters belong to the planner, not per-company enrichment.
619
624
  const catalogue = catalogRow && CATALOGUE_CARD_IDS.has(card.id) && !isCompanySourcingIntent(normalized)
620
625
  ? catalogueRecommendations(catalogRow, card) : null;
621
- return {
626
+ return withRecordBrief({
622
627
  card: routeCardFor(card, normalized),
623
628
  recommendedTools: recommendations.tools,
624
629
  recommendedCommands: recommendations.commands,
@@ -627,7 +632,7 @@ export function inferCapabilityRoute(query) {
627
632
  ...(catalogue && catalogRow
628
633
  ? { catalogIntent: { id: catalogRow.id, label: catalogRow.label, status: catalogRow.status, group: catalogRow.group, tools: catalogue.tools, commands: catalogue.commands, exactCommand: catalogue.exactCommand, exactMcpCall: catalogue.exactMcpCall } }
629
634
  : {}),
630
- };
635
+ }, networkMiningEnabled(runtimeEnv()));
631
636
  }
632
637
  /**
633
638
  * A data-type ask ("tech stack", "decision makers at each account", "funding
@@ -698,6 +703,7 @@ export function inferPrimitiveRoute(query) {
698
703
  export const RETIRED_PRODUCT_SKILLS = Object.freeze(["oxygen-playbooks", "oxygen-visual-design", "oxygen-linkedin-marketing"]);
699
704
  /** Current hosted instruction audiences, shared by discovery and Agent snapshots. */
700
705
  export const OXYGEN_PRODUCT_SKILL_AUDIENCES = Object.freeze({
706
+ "oxygen-reply-oxygent": "internal_dev",
701
707
  "oxygen-geo": "internal_dev",
702
708
  "oxygen-onboarding": "workspace",
703
709
  "oxygen-gtm-consultant": "workspace",
@@ -1000,6 +1006,84 @@ function crmConnectionsReadRecommendation(query) {
1000
1006
  execution: "Read stored conversations and Sequence links for one CRM record with oxygen_crm_record_connections. Request projection=compact and a max_chars window; conversation_limit and sequence_limit bound the recent links. hasMore includes older or omitted links; this read has no cursor for older links and does not sync Messages, contact anyone or spend credits.",
1001
1007
  };
1002
1008
  }
1009
+ /**
1010
+ * "Tell me everything about this person" is a stored-Records read. Before this
1011
+ * rule it matched no card, and lexical search ranked a write tool
1012
+ * (oxygen_profile_viewers_enrichment) first. Every recommended tool is free.
1013
+ */
1014
+ function recordDossierReadRecommendation(query) {
1015
+ const recordNoun = "(?:person|people|lead|leads|contact|contacts|prospect|prospects|account|accounts|customer|buyer|record|company|someone)";
1016
+ const subject = new RegExp(`\\b(?:this|that|the|my|our|a|one)\\s+(?:${recordNoun})\\b`).test(query)
1017
+ || /\b(?:them|him|her|they)\b/.test(query)
1018
+ || /\b(?:know|knows|have|has|got)\s+(?:about|on)\s+(?!our\b|my\b|the\b|us\b)[a-z]+(?:\s+[a-z]+)?\s*\??$/.test(query)
1019
+ || /\b(?:call|meeting|chat)\s+with\s+[a-z]+(?:\s+[a-z]+)?\s*\??$/.test(query);
1020
+ // "profile, timeline, notes, conversations, meetings": several facets of one record at once.
1021
+ const facets = query.match(/\b(?:profile|timeline|activity|notes?|signals?|conversations?|messages|sequences?|meetings?|calls?|tasks?|related|connections?)\b/g) ?? [];
1022
+ const enumerated = new Set(facets.map((facet) => facet.replace(/s$/, ""))).size >= 3
1023
+ && new RegExp(`\\b(?:${recordNoun})\\b`).test(query);
1024
+ const dossier = enumerated
1025
+ || /\beverything\s+(?:about|on|we\s+(?:know|have))\b/.test(query)
1026
+ || /\beverything\b.{0,40}\b(?:knows?|has|have|stores?|stored)\b(?:.{0,12}\b(?:about|on)\b)?/.test(query)
1027
+ || /\bwhat\s+(?:do|does|did)\s+(?:we|i|you|oxygen|the\s+workspace)\s+(?:already\s+)?know\b/.test(query)
1028
+ || /\ball\s+(?:the\s+)?(?:info|information|data|details|context)\s+(?:on|about|for)\b/.test(query)
1029
+ || /\bfull\s+(?:picture|story|history|context)\b/.test(query)
1030
+ || /\b(?:prep|prepare)\s+(?:me\s+)?for\s+(?:a\s+|my\s+|the\s+)?(?:call|meeting|chat)\s+with\b/.test(query)
1031
+ || /\b(?:full|complete|quick|short)?\s*(?:brief|briefing|dossier|background|profile|summary|overview)\s+(?:on|of|for|about)\b/.test(query)
1032
+ || /\b(?:summari[sz]e|brief me on|tell me about|catch me up on|background on)\b/.test(query);
1033
+ if (!subject && !enumerated)
1034
+ return null;
1035
+ if (!dossier)
1036
+ return null;
1037
+ if (isHostedWorkflowIntent(query)
1038
+ || /\b(?:send\w*|repl(?:y|ies)|draft\w*|writ\w*|enroll\w*|enrich\w*|find (?:their |the |a )?(?:email|phone|mobile)|scrape\w*|creat\w*|updat\w*|edit\w*|merg\w*|delet\w*|remov\w*|import\w*|sync\w*|tag\w*)\b/.test(query))
1039
+ return null;
1040
+ // Naming another primitive's object is a different owner, unless the ask
1041
+ // enumerates one record's facets ("its notes, conversations, sequences").
1042
+ if (!enumerated && /\b(?:icp|ideal customer|positioning|offers?|brand|voice|meetings?|calls? (?:transcript|recording)s?|tables?|rows?|columns?|spreadsheets?|workflows?|agents?|sequences?|campaigns?|dashboards?|reports?|billing|credits?)\b/.test(query))
1043
+ return null;
1044
+ return {
1045
+ tools: [
1046
+ "oxygen_crm_record_search", "oxygen_crm_record_get", "oxygen_crm_record_timeline",
1047
+ "oxygen_crm_record_connections", "oxygen_crm_notes_list", "oxygen_crm_record_related",
1048
+ ],
1049
+ commands: ["crm search", "crm get", "crm activity timeline", "crm connections", "crm notes list", "crm related"],
1050
+ execution: "Read what the workspace already stores about one record: resolve it with search (a name, email, domain or LinkedIn URL), then get its fields and identities, its timeline (signals, calls, meetings, notes), connected conversations and Sequences, notes and related records. Every read is free and contacts no one. Finding missing contact data or enriching is a separate paid action. External MCP uses oxygen_capabilities_read for unlisted reads; built-in agents invoke the native tools.",
1051
+ };
1052
+ }
1053
+ const RECORD_DOSSIER_FIRST_COMMANDS = ["crm search", "crm get", "crm activity timeline"];
1054
+ const RECORD_BRIEF_TOOL = "oxygen_crm_record_brief";
1055
+ const RECORD_BRIEF_COMMAND = "crm brief";
1056
+ const RECORD_BRIEF_EXECUTION_PREFIX = `Start with ${RECORD_BRIEF_COMMAND} (${RECORD_BRIEF_TOOL}): one free read of everything stored about the record, with a cursor to each section's rest. `;
1057
+ /**
1058
+ * Put the Network Mining record brief first on a record-dossier route where the
1059
+ * host serves it, and take it off where it does not. The shared router decides
1060
+ * by `networkMiningEnabled` in its own process; the CLI decides again by its
1061
+ * API host, because its local environment says nothing about the server's.
1062
+ * Idempotent; any other route is returned unchanged.
1063
+ */
1064
+ export function withRecordBrief(route, served) {
1065
+ if (route.card.id !== "records")
1066
+ return route;
1067
+ const briefed = route.recommendedTools[0] === RECORD_BRIEF_TOOL;
1068
+ if (!served) {
1069
+ if (!briefed)
1070
+ return route;
1071
+ return {
1072
+ ...route,
1073
+ card: { ...route.card, execution: route.card.execution.replace(RECORD_BRIEF_EXECUTION_PREFIX, "") },
1074
+ recommendedTools: route.recommendedTools.slice(1),
1075
+ recommendedCommands: route.recommendedCommands.filter((command) => command !== RECORD_BRIEF_COMMAND),
1076
+ };
1077
+ }
1078
+ if (briefed || !RECORD_DOSSIER_FIRST_COMMANDS.every((command, index) => route.recommendedCommands[index] === command))
1079
+ return route;
1080
+ return {
1081
+ ...route,
1082
+ card: { ...route.card, execution: `${RECORD_BRIEF_EXECUTION_PREFIX}${route.card.execution}` },
1083
+ recommendedTools: [RECORD_BRIEF_TOOL, ...route.recommendedTools],
1084
+ recommendedCommands: [RECORD_BRIEF_COMMAND, ...route.recommendedCommands],
1085
+ };
1086
+ }
1003
1087
  /** Record-file lifecycle asks outrank the generic workspace label vocabulary. */
1004
1088
  function crmFileUploadRecommendation(query) {
1005
1089
  // Literal filenames and record identities are values, not competing owners:
@@ -1167,6 +1251,9 @@ function routeCardFor(card, query) {
1167
1251
  const crmConnections = card.id === "records" ? crmConnectionsReadRecommendation(query) : null;
1168
1252
  if (crmConnections)
1169
1253
  return { ...card, posture: "read_only", execution: crmConnections.execution };
1254
+ const dossier = card.id === "records" ? recordDossierReadRecommendation(query) : null;
1255
+ if (dossier)
1256
+ return { ...card, posture: "read_only", execution: dossier.execution };
1170
1257
  const crmFileUpload = card.id === "records" ? crmFileUploadRecommendation(query) : null;
1171
1258
  if (crmFileUpload)
1172
1259
  return { ...card, execution: crmFileUpload.execution };
@@ -1553,9 +1640,26 @@ function isOxygentChatIntent(query) {
1553
1640
  return true;
1554
1641
  if (/\b(chatgpt|claude|codex|oauth|mcp|connectors?|connected apps?)\b/.test(query))
1555
1642
  return false;
1556
- const chatPrivacy = /\b(chats?|conversations?|histor(?:y|ies)|visibility|shared|private|personal)\b/.test(query);
1557
- if (/\bai (?:assistants?|teammates?)\b/.test(query))
1558
- return chatPrivacy;
1643
+ const conversation = /\b(chats?|conversations?|histor(?:y|ies))\b/.test(query);
1644
+ const assistantAudience = /\b(?:agents?|assistants?|teammates?)(?:'s)? visibility\b/.test(query)
1645
+ || /\bvisibility (?:of|for) (?:(?:my|our|the|an?) )?(?:ai )?(?:agents?|assistants?|teammates?)\b/.test(query)
1646
+ // An audience phrase ends here or introduces a relative clause.
1647
+ // Trailing inputs such as "workspace email enrichment" keep their owner.
1648
+ || /\b(?:assistants?|teammates?) for (?:(?:my|our|the|a) )?(?:(?:personal|private) use|(?:shared|team) workspace(?=$|[,.!?;:)]| (?:that|which|where|with)\b))\b/.test(query)
1649
+ || /\b(?:private|personal|shared) (?:ai )?(?:agents?|assistants?|teammates?|chats?|conversations?)\b/.test(query);
1650
+ // "Run my private agent only once" is execution, not creator-only access.
1651
+ // An execution request changes owner only when it also names a visibility edit.
1652
+ if (/\bagents?\b/.test(query) && /\b(?:run|execut\w*)\b/.test(query)
1653
+ && !(assistantAudience && /\bvisibility\b/.test(query) && /\b(?:chang\w*|updat\w*|configur\w*|set)\b/.test(query)))
1654
+ return false;
1655
+ if (/\bai (?:agents? )?(?:assistants?|teammates?)\b/.test(query)) {
1656
+ return (conversation || assistantAudience) && (!/\b(?:run|execut\w*)\b/.test(query) || assistantAudience);
1657
+ }
1658
+ // Privacy of an Agent itself belongs to Oxygents, not custom Agent runs.
1659
+ // Keep ordinary run requests and private input data on the Agent gateway.
1660
+ if (/\bagents?\b/.test(query) && assistantAudience
1661
+ && /\b(?:visibility|only|creat\w*|make|new|privacy|configur\w*|settings?)\b/.test(query))
1662
+ return true;
1559
1663
  // Human membership asks must not re-enter Oxygent mutations through the
1560
1664
  // generic fallback after the user-facing routine router excludes them.
1561
1665
  // Explicit Oxygent/AI teammate access above keeps its native owner.
@@ -2087,6 +2191,8 @@ function explicitCapabilityIntent(query) {
2087
2191
  return ROUTE_BY_ID.get("records") ?? null;
2088
2192
  if (crmConnectionsReadRecommendation(query))
2089
2193
  return ROUTE_BY_ID.get("records") ?? null;
2194
+ if (recordDossierReadRecommendation(query))
2195
+ return ROUTE_BY_ID.get("records") ?? null;
2090
2196
  if (crmFileUploadRecommendation(query))
2091
2197
  return ROUTE_BY_ID.get("records") ?? null;
2092
2198
  if (crmMaintenanceRecommendation(query))
@@ -2461,6 +2567,9 @@ function recommendationsFor(card, query) {
2461
2567
  const crmConnections = card.id === "records" ? crmConnectionsReadRecommendation(query) : null;
2462
2568
  if (crmConnections)
2463
2569
  return crmConnections;
2570
+ const dossier = card.id === "records" ? recordDossierReadRecommendation(query) : null;
2571
+ if (dossier)
2572
+ return dossier;
2464
2573
  const crmSample = card.id === "records" ? crmSampleRecordRecommendation(query) : null;
2465
2574
  const crmFileUpload = card.id === "records" ? crmFileUploadRecommendation(query) : null;
2466
2575
  if (crmFileUpload)
@@ -31,7 +31,7 @@ export const CUSTOMER_OPERATIONS = [
31
31
  operation("activation.state", "/api/cli/activation/state", "GET", "activation state", "oxygen_activation_state", "workspace", "Read this workspace's onboarding activation state and next available actions.", {}, [], mcpOnly),
32
32
  operation("activation.checklist", "/api/cli/activation/checklist", "GET", "activation checklist", "oxygen_activation_checklist", "workspace", "Read this workspace's getting-started checklist: leads in a table, a first sequence, a connected sending account and a launched sequence, each with whether it is done and a link to do it; eligible says whether the sidebar shows it. Read-only, 0 credits."),
33
33
  operation("api_keys.list", "/api/cli/api-keys", "GET", "api-keys list", "oxygen_api_keys_list", "workspace", "List workspace API keys without revealing their tokens.", {}, [], mcpOnly),
34
- operation("api_keys.create", "/api/cli/api-keys", "POST", "api-keys create", "oxygen_api_keys_create", "workspace", "Create a workspace API key; requires the caller's personal user session. The token is returned once.", { name: text(), expires_at: text("ISO timestamp; omit for no expiry.") }, [], mcpOnly),
34
+ operation("api_keys.create", "/api/cli/api-keys", "POST", "api-keys create", "oxygen_api_keys_create", "workspace", "Create a workspace API key; requires the caller's personal user session. The token is returned once.", { name: text(), expires_at: text("ISO timestamp; omit for no expiry."), access: enumOf(["full", "read_only"]) }, [], mcpOnly),
35
35
  operation("api_keys.revoke", "/api/cli/api-keys/{key_id}", "DELETE", "api-keys revoke", "oxygen_api_keys_revoke", "workspace", "Revoke a workspace API key; requires the caller's personal user session.", {}, [], mcpOnly),
36
36
  operation("comments.edit", "/api/cli/collab/comments/{comment_id}", "PATCH", "comments edit", "oxygen_comments_edit", "collaboration", "Edit your own internal workspace comment; records the edit time.", { body: text() }, ["body"], mcpOnly),
37
37
  operation("comments.delete", "/api/cli/collab/comments/{comment_id}", "DELETE", "comments delete", "oxygen_comments_delete", "collaboration", "Retract your own internal workspace comment while retaining its audit trail.", {}, [], mcpOnly),