@oxygen-agent/cli 1.906.0 → 1.922.12

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 (29) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +13 -1
  3. package/dist/index.js +337 -40
  4. package/node_modules/@oxygen/formula/dist/expression.d.ts +21 -0
  5. package/node_modules/@oxygen/formula/dist/expression.js +42 -1
  6. package/node_modules/@oxygen/formula/dist/formula-functions.d.ts +1 -1
  7. package/node_modules/@oxygen/formula/dist/formula-functions.js +10 -1
  8. package/node_modules/@oxygen/shared/dist/billing.d.ts +17 -0
  9. package/node_modules/@oxygen/shared/dist/billing.js +20 -0
  10. package/node_modules/@oxygen/shared/dist/capability-discovery.js +12 -7
  11. package/node_modules/@oxygen/shared/dist/copilot-errors.d.ts +1 -0
  12. package/node_modules/@oxygen/shared/dist/copilot-errors.js +9 -0
  13. package/node_modules/@oxygen/shared/dist/copilot-plan.d.ts +39 -0
  14. package/node_modules/@oxygen/shared/dist/copilot-plan.js +76 -7
  15. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +36 -0
  16. package/node_modules/@oxygen/shared/dist/langfuse.js +83 -0
  17. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.d.ts +1 -1
  18. package/node_modules/@oxygen/shared/dist/linkedin-quota-denial.js +16 -5
  19. package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +22 -0
  20. package/node_modules/@oxygen/shared/dist/linkedin-url.js +104 -0
  21. package/node_modules/@oxygen/shared/dist/provider-funding-errors.d.ts +16 -2
  22. package/node_modules/@oxygen/shared/dist/provider-funding-errors.js +19 -9
  23. package/node_modules/@oxygen/shared/dist/table-limits.d.ts +18 -0
  24. package/node_modules/@oxygen/shared/dist/table-limits.js +18 -0
  25. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +23 -2
  26. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  27. package/node_modules/@oxygen/shared/dist/version.js +1 -1
  28. package/node_modules/@oxygen/shared/package.json +10 -0
  29. package/package.json +2 -1
@@ -25,6 +25,28 @@ export type LinkedinUrlNormalization = {
25
25
  dispatchUrl: string;
26
26
  } | null;
27
27
  export declare function normalizeLinkedinProfileUrl(raw: string | null | undefined): LinkedinUrlNormalization;
28
+ /**
29
+ * Canonicalize any LinkedIn PROFILE REFERENCE a customer might put in a cell:
30
+ * a full `/in/` URL, a scheme-less `linkedin.com/in/…`, or a BARE public handle
31
+ * (`peter-neumann-b25a6717b`, `christopherueink1`) that people-search exports
32
+ * and manual research routinely produce. Purely lexical — no provider call.
33
+ *
34
+ * The rule, in order:
35
+ * 1. Anything `normalizeLinkedinProfileUrl` accepts wins (URL forms).
36
+ * 2. A URL-shaped value it rejected (company page, Sales Navigator, a
37
+ * non-LinkedIn host) stays rejected — never guessed at.
38
+ * 3. An `ACo…`/`ACw…` member provider id is NOT a handle. It returns null
39
+ * because this function's contract is "give me a profile URL"; a caller
40
+ * that accepts member ids must recognize them itself and pass them through
41
+ * as an id. Fabricating `/in/ACoAA…` would address nobody.
42
+ * 4. What is left is a handle only if it matches the public-identifier charset
43
+ * AND contains a letter AND contains a hyphen or digit AND is not a
44
+ * placeholder token. Everything else returns null.
45
+ *
46
+ * A canonicalized handle is dispatchable the same way a URL is: send-time
47
+ * resolution (`users_get`) turns `/in/<handle>` into the real member id.
48
+ */
49
+ export declare function canonicalizeLinkedinProfileReference(raw: string | null | undefined): LinkedinUrlNormalization;
28
50
  /**
29
51
  * Extract the addressable activity id from a LinkedIn post URL. Returns null for
30
52
  * a bare id (already addressable — pass it through), and null for a linkedin.com
@@ -65,6 +65,110 @@ export function normalizeLinkedinProfileUrl(raw) {
65
65
  dispatchUrl: `https://www.linkedin.com/in/${decoded}`,
66
66
  };
67
67
  }
68
+ // A LinkedIn member PROVIDER id (Unipile `ACoAA…` / `ACwAA…`) is base64url of a
69
+ // member URN, not a public identifier — rewriting one into `/in/<id>` would send
70
+ // a nonexistent profile URL to the provider. Guard the whole PREFIX rather than a
71
+ // length-qualified shape: a short or truncated id (`ACo123`) would otherwise slip
72
+ // through and be dispatched as if it were a handle. LinkedIn mints public slugs
73
+ // lowercase, so `acoustics-lab` is a real handle; the guard only fires on a
74
+ // leading uppercase `AC`, and it tolerates the third letter being upper-cased
75
+ // too (`ACOAA…`, the shape a spreadsheet UPPER() or a CRM export produces), so
76
+ // such an id is named as unrecognized at enrollment instead of dying at send.
77
+ const LINKEDIN_MEMBER_ID_PREFIX_RE = /^AC[OoWw]/;
78
+ // LinkedIn's public identifier ("vanity slug") charset: letters, digits and
79
+ // hyphens only, 3–100 characters, never leading with a hyphen. Deliberately
80
+ // NARROWER than "any non-URL string" — no `.`, `@`, `_`, `:`, `/` or whitespace
81
+ // — so an email (`someone@example.com`), a bare domain (`example.com`), and a
82
+ // free-text research placeholder can never be mistaken for a handle and turned
83
+ // into a dispatchable profile URL. Unicode letters/digits are allowed because
84
+ // `normalizeLinkedinProfileUrl` already percent-decodes non-ASCII slugs.
85
+ const LINKEDIN_PUBLIC_HANDLE_RE = /^[\p{L}\p{N}][\p{L}\p{N}-]{2,99}$/u;
86
+ /**
87
+ * A bare handle is only accepted when it carries a hyphen or a digit — the
88
+ * `firstname-lastname-hash` / `name1` shapes LinkedIn actually mints and people
89
+ * search actually exports.
90
+ *
91
+ * THIS IS THE STRANGER-DISPATCH GUARD. Without it, every single plain word in a
92
+ * mis-mapped column becomes a dispatchable profile: `alexander`, `Schmidt`,
93
+ * `acme`, `microsoft`, `unknown`, `pending` all resolve to a REAL LinkedIn
94
+ * member who has nothing to do with the row, and outreach goes to a stranger
95
+ * under the customer's own account. A wrongly-skipped row is recoverable and is
96
+ * reported by name; a message to the wrong human is not. The cost is that a real
97
+ * separator-free vanity handle (`satyanadella`) must be supplied as a full URL —
98
+ * which the per-row `unrecognized_linkedin_identifier` detail tells the customer.
99
+ */
100
+ const LINKEDIN_HANDLE_REQUIRES_SEPARATOR_OR_DIGIT = /[-\p{N}]/u;
101
+ /** A handle must still contain a letter, so a phone number (`4915112345678`) is not one. */
102
+ const LINKEDIN_HANDLE_REQUIRES_LETTER = /\p{L}/u;
103
+ /**
104
+ * Values that pass the shape tests but are obviously a spreadsheet's way of
105
+ * saying "no value". They are compared case-insensitively against the whole
106
+ * trimmed cell WITH ITS HYPHENS REMOVED, so the hyphenated spellings that would
107
+ * otherwise satisfy the separator rule (`n-a`, `not-found`, `no-linkedin`,
108
+ * `url-to-confirm`) are caught by the same list as their plain forms.
109
+ */
110
+ const LINKEDIN_HANDLE_PLACEHOLDER_TOKENS = new Set([
111
+ "none", "null", "nil", "undefined", "unknown", "missing", "pending", "tbd",
112
+ "todo", "error", "failed", "true", "false", "empty", "blank", "test", "na",
113
+ "notfound", "nolinkedin", "notonlinkedin", "noprofile", "nourl", "notavailable",
114
+ "notapplicable", "unavailable", "urltoconfirm", "tobeconfirmed", "toconfirm",
115
+ "placeholder", "sample", "example", "dummy", "unresolved",
116
+ ]);
117
+ function isLinkedinHandlePlaceholder(value) {
118
+ return LINKEDIN_HANDLE_PLACEHOLDER_TOKENS.has(value.toLowerCase().replace(/-/g, ""));
119
+ }
120
+ /**
121
+ * Canonicalize any LinkedIn PROFILE REFERENCE a customer might put in a cell:
122
+ * a full `/in/` URL, a scheme-less `linkedin.com/in/…`, or a BARE public handle
123
+ * (`peter-neumann-b25a6717b`, `christopherueink1`) that people-search exports
124
+ * and manual research routinely produce. Purely lexical — no provider call.
125
+ *
126
+ * The rule, in order:
127
+ * 1. Anything `normalizeLinkedinProfileUrl` accepts wins (URL forms).
128
+ * 2. A URL-shaped value it rejected (company page, Sales Navigator, a
129
+ * non-LinkedIn host) stays rejected — never guessed at.
130
+ * 3. An `ACo…`/`ACw…` member provider id is NOT a handle. It returns null
131
+ * because this function's contract is "give me a profile URL"; a caller
132
+ * that accepts member ids must recognize them itself and pass them through
133
+ * as an id. Fabricating `/in/ACoAA…` would address nobody.
134
+ * 4. What is left is a handle only if it matches the public-identifier charset
135
+ * AND contains a letter AND contains a hyphen or digit AND is not a
136
+ * placeholder token. Everything else returns null.
137
+ *
138
+ * A canonicalized handle is dispatchable the same way a URL is: send-time
139
+ * resolution (`users_get`) turns `/in/<handle>` into the real member id.
140
+ */
141
+ export function canonicalizeLinkedinProfileReference(raw) {
142
+ const fromUrl = normalizeLinkedinProfileUrl(raw);
143
+ if (fromUrl)
144
+ return fromUrl;
145
+ if (typeof raw !== "string")
146
+ return null;
147
+ const trimmed = raw.trim();
148
+ if (!trimmed)
149
+ return null;
150
+ // A URL we could not reduce to a profile is a rejection, not a handle.
151
+ if (looksLikeUrlIdentifier(trimmed))
152
+ return null;
153
+ if (LINKEDIN_MEMBER_ID_PREFIX_RE.test(trimmed))
154
+ return null;
155
+ if (!LINKEDIN_PUBLIC_HANDLE_RE.test(trimmed))
156
+ return null;
157
+ if (!LINKEDIN_HANDLE_REQUIRES_LETTER.test(trimmed))
158
+ return null;
159
+ if (!LINKEDIN_HANDLE_REQUIRES_SEPARATOR_OR_DIGIT.test(trimmed))
160
+ return null;
161
+ if (isLinkedinHandlePlaceholder(trimmed))
162
+ return null;
163
+ const handle = trimmed.toLowerCase();
164
+ return {
165
+ normalized: `https://www.linkedin.com/in/${handle}`,
166
+ handle,
167
+ // dispatchHandle keeps the original case, exactly as the URL path does.
168
+ dispatchHandle: trimmed,
169
+ dispatchUrl: `https://www.linkedin.com/in/${trimmed}`,
170
+ };
171
+ }
68
172
  // A LinkedIn post URL carries the numeric activity id we address the post by:
69
173
  // /posts/{slug}-activity-{19 digits}-{4 chars}
70
174
  // /feed/update/urn:li:activity:{19 digits}
@@ -40,5 +40,19 @@ export declare function isProviderFundingErrorCode(code: string | null | undefin
40
40
  * would otherwise read as retryable.
41
41
  */
42
42
  export declare function isProviderRateLimitErrorCode(code: string | null | undefined): boolean;
43
- /** The customer-facing next step for a funding refusal, by credential ownership. */
44
- export declare function providerFundingNextStep(credentialMode: "managed" | "byok" | null | undefined): string;
43
+ /**
44
+ * True when the credential the provider refused belongs to the CUSTOMER rather
45
+ * than to Oxygen's managed pool.
46
+ *
47
+ * The two halves of a 402 need opposite copy and opposite promises. Oxygen's own
48
+ * dry account is our outage: the breaker benches the provider, the row is
49
+ * preserved, and the work resumes once ops funds it. A customer-owned account is
50
+ * theirs to top up: nothing resumes, nobody is alerted, and telling them to wait
51
+ * is how one workspace re-ran the same column every few hours for nine days.
52
+ *
53
+ * The producers spell customer ownership four ways (`byok` for an explicit
54
+ * bring-your-own key, `user_oauth` / `user_api_key` / `user_connection` for a
55
+ * connected account) — every one of them is the customer's money, so the
56
+ * predicate is "not managed", not "equals byok".
57
+ */
58
+ export declare function isCustomerOwnedCredentialMode(credentialMode: string | null | undefined): boolean;
@@ -69,13 +69,23 @@ export function isProviderRateLimitErrorCode(code) {
69
69
  return false;
70
70
  return /rate|limit|429|capacity_deferred/i.test(code);
71
71
  }
72
- /** The customer-facing next step for a funding refusal, by credential ownership. */
73
- export function providerFundingNextStep(credentialMode) {
74
- if (credentialMode === "byok") {
75
- return "Top up or upgrade the provider account behind your connected key, then retry. Waiting will not clear this.";
76
- }
77
- if (credentialMode === "managed") {
78
- return "This is OXYGEN's managed provider account, not your credit balance — contact support. Waiting will not clear this; route the run to another provider in the meantime.";
79
- }
80
- return "Check the provider account's balance and plan entitlement, then retry. Waiting will not clear this.";
72
+ /**
73
+ * True when the credential the provider refused belongs to the CUSTOMER rather
74
+ * than to Oxygen's managed pool.
75
+ *
76
+ * The two halves of a 402 need opposite copy and opposite promises. Oxygen's own
77
+ * dry account is our outage: the breaker benches the provider, the row is
78
+ * preserved, and the work resumes once ops funds it. A customer-owned account is
79
+ * theirs to top up: nothing resumes, nobody is alerted, and telling them to wait
80
+ * is how one workspace re-ran the same column every few hours for nine days.
81
+ *
82
+ * The producers spell customer ownership four ways (`byok` for an explicit
83
+ * bring-your-own key, `user_oauth` / `user_api_key` / `user_connection` for a
84
+ * connected account) — every one of them is the customer's money, so the
85
+ * predicate is "not managed", not "equals byok".
86
+ */
87
+ export function isCustomerOwnedCredentialMode(credentialMode) {
88
+ if (!credentialMode)
89
+ return false;
90
+ return credentialMode.trim().toLowerCase() !== "managed";
81
91
  }
@@ -1,2 +1,20 @@
1
1
  export declare const MAX_TABLE_ACTION_RUN_ROWS = 500000;
2
2
  export declare const MAX_WORKSPACE_ROW_DELETE_ROWS = 50000;
3
+ /**
4
+ * Platform ceiling for a table action run's `max_concurrency` (T-27).
5
+ *
6
+ * The API used to accept 1..1000 and silently throttle anything the worker could
7
+ * not serve (the August incident: a customer set 200-250 and watched it behave
8
+ * nothing like the number they chose). This is the honest upper bound: 250, the
9
+ * highest per-run in-flight ceiling ANY lane genuinely serves — the set-wise
10
+ * link_batch lane (LINK_BATCH_ROWS = 250 in the worker scheduler). No run type is
11
+ * validated below its real cap.
12
+ *
13
+ * It is deliberately NOT the external-tool provider feeder cap
14
+ * (EXTERNAL_TOOL_RUN_FEEDER_CAP = 160, an independent constant in the worker):
15
+ * an external_tool run is accepted up to this ceiling and the worker bounds it at
16
+ * 160 in-flight, and the create envelope reports that lower effective ceiling
17
+ * honestly (max_concurrency_platform_ceiling / max_concurrency_provider_ceiling)
18
+ * rather than under-serving link runs to make one flat number "true".
19
+ */
20
+ export declare const MAX_TABLE_ACTION_RUN_MAX_CONCURRENCY = 250;
@@ -2,3 +2,21 @@
2
2
  // surface that resolves a symbolic row selection before invoking it.
3
3
  export const MAX_TABLE_ACTION_RUN_ROWS = 500_000;
4
4
  export const MAX_WORKSPACE_ROW_DELETE_ROWS = 50_000;
5
+ /**
6
+ * Platform ceiling for a table action run's `max_concurrency` (T-27).
7
+ *
8
+ * The API used to accept 1..1000 and silently throttle anything the worker could
9
+ * not serve (the August incident: a customer set 200-250 and watched it behave
10
+ * nothing like the number they chose). This is the honest upper bound: 250, the
11
+ * highest per-run in-flight ceiling ANY lane genuinely serves — the set-wise
12
+ * link_batch lane (LINK_BATCH_ROWS = 250 in the worker scheduler). No run type is
13
+ * validated below its real cap.
14
+ *
15
+ * It is deliberately NOT the external-tool provider feeder cap
16
+ * (EXTERNAL_TOOL_RUN_FEEDER_CAP = 160, an independent constant in the worker):
17
+ * an external_tool run is accepted up to this ceiling and the worker bounds it at
18
+ * 160 in-flight, and the create envelope reports that lower effective ceiling
19
+ * honestly (max_concurrency_platform_ceiling / max_concurrency_provider_ceiling)
20
+ * rather than under-serving link runs to make one flat number "true".
21
+ */
22
+ export const MAX_TABLE_ACTION_RUN_MAX_CONCURRENCY = 250;
@@ -1,4 +1,4 @@
1
- import { getCapabilityRouteMatch, inferCapabilityRoute, } from "./capability-discovery.js";
1
+ import { getCapabilityRoute, getCapabilityRouteMatch, inferCapabilityRoute, } from "./capability-discovery.js";
2
2
  // Provider brands are intentionally not duplicated into the static capability
3
3
  // catalog. A short "connect <provider>" query would otherwise match Tables via
4
4
  // its relationship-oriented "connect" term. Correct that narrow ambiguity at
@@ -13,9 +13,30 @@ export function inferUserCapabilityRoute(query) {
13
13
  }
14
14
  return getCapabilityRouteMatch("connected-integrations") ?? route;
15
15
  }
16
+ // The words that mean "this is Tables work, not a provider connection". Hand-typing
17
+ // them drifted: the list covered table/row/column/dataset/join/relate but not csv,
18
+ // import, enrich, waterfall, formula, lookup or score -- all of which the Tables card
19
+ // itself claims. The measured cost was 11 queries ("connect my csv", "connect my
20
+ // enrichment", "connect my formula") losing their Tables tools and being handed
21
+ // oxygen_integrations_connect instead, on ALL THREE surfaces that call this wrapper.
22
+ //
23
+ // So derive from the card rather than restating it. `connect` is excluded because it
24
+ // is the ambiguity itself -- it is a Tables intent term AND the verb this function
25
+ // keys on, and leaving it in would make the guard reject every query the wrapper
26
+ // exists to correct.
27
+ const TABLES_INTENT_AMBIGUITY_TRIGGERS = new Set(["connect"]);
28
+ function tablesIntentGuard() {
29
+ const terms = (getCapabilityRoute("tables")?.intentTerms ?? [])
30
+ .filter((term) => !TABLES_INTENT_AMBIGUITY_TRIGGERS.has(term))
31
+ .flatMap((term) => (term.includes(" ") ? [term] : [term, `${term}s`]))
32
+ .map((term) => term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))
33
+ .sort((a, b) => b.length - a.length);
34
+ return new RegExp(`\\b(?:${terms.join("|")})\\b`);
35
+ }
36
+ const TABLES_INTENT_GUARD = tablesIntentGuard();
16
37
  function isSimpleProviderConnectionIntent(query) {
17
38
  const normalized = query.toLowerCase().replace(/[_-]+/g, " ").replace(/\s+/g, " ").trim();
18
- if (/\b(table|tables|row|rows|column|columns|dataset|join|relationship|relate)\b/.test(normalized)) {
39
+ if (TABLES_INTENT_GUARD.test(normalized)) {
19
40
  return false;
20
41
  }
21
42
  if (/\b(integration|provider|oauth|byok|api key|connected account)\b/.test(normalized)) {
@@ -1,4 +1,4 @@
1
- export declare const OXYGEN_VERSION = "1.906.0";
1
+ export declare const OXYGEN_VERSION = "1.922.12";
2
2
  export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
3
3
  export declare const MANAGED_INBOX_MINIMUM_CLI_VERSION = "1.326.2";
4
4
  export declare const SUPPORT_AGENT_REPLY_MINIMUM_CLI_VERSION = "1.747.0";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.906.0";
1
+ export const OXYGEN_VERSION = "1.922.12";
2
2
  // The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
3
3
  // operational route. Raising it hard-rejects every older CLI from the entire
4
4
  // product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
@@ -96,6 +96,11 @@
96
96
  "import": "./dist/dnc-identities.js",
97
97
  "default": "./dist/dnc-identities.js"
98
98
  },
99
+ "./provider-funding-errors": {
100
+ "types": "./dist/provider-funding-errors.d.ts",
101
+ "import": "./dist/provider-funding-errors.js",
102
+ "default": "./dist/provider-funding-errors.js"
103
+ },
99
104
  "./cli-result": {
100
105
  "types": "./dist/cli-result.d.ts",
101
106
  "import": "./dist/cli-result.js",
@@ -141,6 +146,11 @@
141
146
  "import": "./dist/pricing-sheet.js",
142
147
  "default": "./dist/pricing-sheet.js"
143
148
  },
149
+ "./copilot-plan": {
150
+ "types": "./dist/copilot-plan.d.ts",
151
+ "import": "./dist/copilot-plan.js",
152
+ "default": "./dist/copilot-plan.js"
153
+ },
144
154
  "./copilot-journeys": {
145
155
  "types": "./dist/copilot-journeys.d.ts",
146
156
  "import": "./dist/copilot-journeys.js",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.906.0",
3
+ "version": "1.922.12",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -34,6 +34,7 @@
34
34
  "dependencies": {
35
35
  "@aws-sdk/client-s3": "3.1050.0",
36
36
  "@aws-sdk/s3-request-presigner": "3.1050.0",
37
+ "@langfuse/core": "^5.11.0",
37
38
  "@langfuse/otel": "^5.11.0",
38
39
  "@langfuse/tracing": "^5.11.0",
39
40
  "@opentelemetry/api": "1.9.1",