@oxygen-agent/cli 1.354.0 → 1.365.3

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.
@@ -8,16 +8,19 @@ export * from "./cli-envelope.js";
8
8
  export * from "./cli-login-code.js";
9
9
  export * from "./cli-result.js";
10
10
  export * from "./column-types.js";
11
+ export * from "./copilot-journeys.js";
11
12
  export * from "./credit-guidance.js";
12
13
  export * from "./directory.js";
13
14
  export * from "./email-tracking-token.js";
14
15
  export * from "./email-unsubscribe-token.js";
15
16
  export * from "./error-redaction.js";
17
+ export * from "./hosted-ai.js";
16
18
  export * from "./identifiers.js";
17
19
  export * from "./knowledge-constants.js";
18
20
  export * from "./knowledge-links.js";
19
21
  export * from "./knowledge-markdown.js";
20
22
  export * from "./knowledge-seed-content.js";
23
+ export * from "./linkedin-mentions.js";
21
24
  export * from "./linkedin-post-url.js";
22
25
  export * from "./linkedin-url.js";
23
26
  export * from "./linkedin-sequences.js";
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Compile LinkedIn post text + stored mention state into Unipile's post
3
+ * contract: the outgoing text carries `{{i}}` placeholders and `mentions[i]`
4
+ * is `{ name, profile_id }`. Anything else (raw `@handle` text next to a
5
+ * mentions array, Oxygen mention-preview snapshots, unresolved handles) is
6
+ * NOT a tagged mention on LinkedIn — the provider either posts it literally
7
+ * or, worse, name-matches entries into the wrong span of text.
8
+ *
9
+ * Accepted inputs per entry (both arrays are optional):
10
+ * - Unipile-shaped: `{ name, profile_id }` (already the wire contract).
11
+ * - Oxygen preview-shaped (publishing mentions resolve output): uses
12
+ * `displayName`/`providerId` (or snake_case), and is only eligible when
13
+ * `status === "resolved"` and `semantics` is `"mention"` (links and plain
14
+ * references stay as typed).
15
+ *
16
+ * Placement: an entry becomes a tagged mention only when its span can be
17
+ * anchored in the text — an existing `{{i}}` placeholder, the entry's `raw`
18
+ * occurrence, or `@name`/`@identifier`. Entries that cannot be anchored are
19
+ * dropped from the outgoing array so the provider can never guess a span.
20
+ */
21
+ export type UnipileLinkedInMention = {
22
+ name: string;
23
+ profile_id: string;
24
+ };
25
+ export type CompiledLinkedInMentions = {
26
+ text: string;
27
+ mentions: UnipileLinkedInMention[];
28
+ dropped: Array<{
29
+ name: string | null;
30
+ reason: "unresolved" | "not_in_text" | "invalid";
31
+ }>;
32
+ };
33
+ export declare function compileLinkedInMentions(input: {
34
+ text: string;
35
+ mentions?: unknown;
36
+ mentionPreviews?: unknown;
37
+ }): CompiledLinkedInMentions;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Compile LinkedIn post text + stored mention state into Unipile's post
3
+ * contract: the outgoing text carries `{{i}}` placeholders and `mentions[i]`
4
+ * is `{ name, profile_id }`. Anything else (raw `@handle` text next to a
5
+ * mentions array, Oxygen mention-preview snapshots, unresolved handles) is
6
+ * NOT a tagged mention on LinkedIn — the provider either posts it literally
7
+ * or, worse, name-matches entries into the wrong span of text.
8
+ *
9
+ * Accepted inputs per entry (both arrays are optional):
10
+ * - Unipile-shaped: `{ name, profile_id }` (already the wire contract).
11
+ * - Oxygen preview-shaped (publishing mentions resolve output): uses
12
+ * `displayName`/`providerId` (or snake_case), and is only eligible when
13
+ * `status === "resolved"` and `semantics` is `"mention"` (links and plain
14
+ * references stay as typed).
15
+ *
16
+ * Placement: an entry becomes a tagged mention only when its span can be
17
+ * anchored in the text — an existing `{{i}}` placeholder, the entry's `raw`
18
+ * occurrence, or `@name`/`@identifier`. Entries that cannot be anchored are
19
+ * dropped from the outgoing array so the provider can never guess a span.
20
+ */
21
+ function isRecord(value) {
22
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
23
+ }
24
+ function readString(record, ...keys) {
25
+ for (const key of keys) {
26
+ const value = record[key];
27
+ if (typeof value === "string" && value.trim())
28
+ return value.trim();
29
+ }
30
+ return null;
31
+ }
32
+ const PLACEHOLDER_RE = /\{\{(\d+)\}\}/g;
33
+ function normalizeEntry(value) {
34
+ if (!isRecord(value))
35
+ return "invalid";
36
+ const status = readString(value, "status");
37
+ const semantics = readString(value, "semantics");
38
+ // Preview-shaped entries: only resolved, mention-semantics entries qualify.
39
+ // Links (full LinkedIn URLs) and plain references stay as typed text.
40
+ if (semantics === "link" || semantics === "plain")
41
+ return "skip";
42
+ if (status && status !== "resolved")
43
+ return "unresolved";
44
+ const name = readString(value, "name", "displayName", "display_name");
45
+ const profileId = readString(value, "profile_id", "providerId", "provider_id");
46
+ if (!name || !profileId)
47
+ return status || semantics ? "unresolved" : "invalid";
48
+ return {
49
+ name,
50
+ profileId,
51
+ raw: readString(value, "raw"),
52
+ identifier: readString(value, "identifier", "publicIdentifier", "public_identifier"),
53
+ };
54
+ }
55
+ /**
56
+ * True when the text already uses `{{i}}` placeholders that all resolve to a
57
+ * valid `{ name, profile_id }` entry — the caller pre-compiled the contract,
58
+ * so text and mentions pass through unchanged.
59
+ */
60
+ function isPrecompiled(text, entries) {
61
+ const indexes = [...text.matchAll(PLACEHOLDER_RE)].map((match) => Number(match[1]));
62
+ if (indexes.length === 0)
63
+ return false;
64
+ return indexes.every((index) => {
65
+ const normalized = normalizeEntry(entries[index]);
66
+ return typeof normalized === "object";
67
+ });
68
+ }
69
+ export function compileLinkedInMentions(input) {
70
+ const explicit = Array.isArray(input.mentions) ? input.mentions : [];
71
+ const previews = Array.isArray(input.mentionPreviews) ? input.mentionPreviews : [];
72
+ if (isPrecompiled(input.text, explicit)) {
73
+ // Compact the array and renumber placeholders together: a non-object
74
+ // entry at an unreferenced index below a referenced one would otherwise
75
+ // shift later entries down while `{{i}}` numbers stay fixed, pointing a
76
+ // placeholder at the wrong (or missing) identity.
77
+ const mentions = [];
78
+ const remap = new Map();
79
+ explicit.forEach((value, oldIndex) => {
80
+ const entry = normalizeEntry(value);
81
+ if (typeof entry !== "object")
82
+ return;
83
+ remap.set(oldIndex, mentions.length);
84
+ mentions.push({ name: entry.name, profile_id: entry.profileId });
85
+ });
86
+ const text = input.text.replace(PLACEHOLDER_RE, (whole, digits) => {
87
+ const next = remap.get(Number(digits));
88
+ return next === undefined ? whole : `{{${next}}}`;
89
+ });
90
+ return { text, mentions, dropped: [] };
91
+ }
92
+ const dropped = [];
93
+ const placed = [];
94
+ let text = input.text;
95
+ const seenProfileIds = new Set();
96
+ for (const value of [...explicit, ...previews]) {
97
+ const entry = normalizeEntry(value);
98
+ if (entry === "skip")
99
+ continue;
100
+ if (entry === "invalid") {
101
+ dropped.push({ name: isRecord(value) ? readString(value, "name", "displayName", "display_name") : null, reason: "invalid" });
102
+ continue;
103
+ }
104
+ if (entry === "unresolved") {
105
+ dropped.push({ name: isRecord(value) ? readString(value, "name", "displayName", "display_name", "raw") : null, reason: "unresolved" });
106
+ continue;
107
+ }
108
+ if (seenProfileIds.has(entry.profileId))
109
+ continue;
110
+ const anchor = findAnchor(text, entry);
111
+ if (!anchor) {
112
+ dropped.push({ name: entry.name, reason: "not_in_text" });
113
+ continue;
114
+ }
115
+ const index = placed.length;
116
+ text = text.slice(0, anchor.start) + `{{${index}}}` + text.slice(anchor.end);
117
+ placed.push({ name: entry.name, profile_id: entry.profileId });
118
+ seenProfileIds.add(entry.profileId);
119
+ }
120
+ return { text, mentions: placed, dropped };
121
+ }
122
+ /**
123
+ * Find the span the mention replaces. Candidates are tried most-specific
124
+ * first: the exact raw capture from resolve time (`@handle`, URN, …), then
125
+ * `@identifier`, then `@name`. Bare names are never matched — replacing an
126
+ * un-prefixed word is how mentions end up covering the wrong text.
127
+ */
128
+ function findAnchor(text, entry) {
129
+ const candidates = [
130
+ entry.raw,
131
+ entry.identifier ? `@${entry.identifier}` : null,
132
+ `@${entry.name}`,
133
+ ].filter((candidate) => Boolean(candidate && candidate.trim()));
134
+ const lowerText = text.toLowerCase();
135
+ for (const candidate of candidates) {
136
+ const lowerCandidate = candidate.toLowerCase();
137
+ // Reject matches that continue into more handle characters so `@acme`
138
+ // can never anchor inside `@acme-labs` — a partial span is exactly the
139
+ // wrong-mention rendering this compiler exists to prevent. A dot only
140
+ // continues the handle when an identifier character follows it
141
+ // (`@acme.io`); a sentence period (`@n8n. Next…`) is a boundary.
142
+ let from = 0;
143
+ while (from <= lowerText.length - lowerCandidate.length) {
144
+ const start = lowerText.indexOf(lowerCandidate, from);
145
+ if (start < 0)
146
+ break;
147
+ const end = start + candidate.length;
148
+ const next = text[end];
149
+ const continues = next !== undefined && (/[a-z0-9_-]/i.test(next)
150
+ || (next === "." && text[end + 1] !== undefined && /[a-z0-9_-]/i.test(text[end + 1])));
151
+ if (!continues)
152
+ return { start, end };
153
+ from = start + 1;
154
+ }
155
+ }
156
+ return null;
157
+ }
@@ -1,5 +1,5 @@
1
1
  /** Primitive kinds that participate in the workspace tag union today. */
2
- export declare const TAG_KINDS: readonly ["knowledge_page", "sequence", "table", "workflow", "recipe"];
2
+ export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe"];
3
3
  export type TagKind = (typeof TAG_KINDS)[number];
4
4
  export declare function isTagKind(value: unknown): value is TagKind;
5
5
  /** Hard cap per tagged item — matches the wiki's long-standing limit. */
@@ -7,6 +7,7 @@
7
7
  /** Primitive kinds that participate in the workspace tag union today. */
8
8
  export const TAG_KINDS = [
9
9
  "knowledge_page",
10
+ "publishing_post",
10
11
  "sequence",
11
12
  "table",
12
13
  "workflow",
@@ -1,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.354.0";
1
+ export declare const OXYGEN_VERSION = "1.365.3";
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";
@@ -1,4 +1,4 @@
1
- export const OXYGEN_VERSION = "1.354.0";
1
+ export const OXYGEN_VERSION = "1.365.3";
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:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.354.0",
3
+ "version": "1.365.3",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",