@oxygen-agent/cli 1.354.0 → 1.377.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.
@@ -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
+ }
@@ -57,9 +57,21 @@ function reserveSurfaceDimension(merged, ctx) {
57
57
  surface: ctx.surface,
58
58
  };
59
59
  }
60
- let logSink = null;
60
+ // The sink registration must live on globalThis, not in a module-local variable:
61
+ // Next.js compiles instrumentation.ts and each route handler into separate
62
+ // bundles, each with its OWN instance of this module. A module-local set by the
63
+ // web log shipper (which starts in instrumentation) is invisible to route
64
+ // handlers — exactly why only startup-context lines (the shipper beacon, the
65
+ // framework console filter) ever shipped flat while 4k+ route-emitted
66
+ // error/warn lines/day stayed drain-only (2026-07-21). globalThis is the one
67
+ // object all bundle copies share; Symbol.for gives every copy the same slot.
68
+ // Same trap + same fix as linkedin-quota's enforcer registration.
69
+ const LOG_SINK_SLOT = Symbol.for("oxygen.log.sink");
61
70
  export function setLogSink(sink) {
62
- logSink = sink;
71
+ globalThis[LOG_SINK_SLOT] = sink;
72
+ }
73
+ function getLogSink() {
74
+ return globalThis[LOG_SINK_SLOT] ?? null;
63
75
  }
64
76
  export function log(level, msg, fields) {
65
77
  const context = getLogContext();
@@ -89,6 +101,7 @@ export function log(level, msg, fields) {
89
101
  else {
90
102
  console.log(line);
91
103
  }
104
+ const logSink = getLogSink();
92
105
  if (!logSink)
93
106
  return;
94
107
  try {
@@ -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", "conversation", "mailbox", "sender", "record"];
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,10 +7,15 @@
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",
13
14
  "recipe",
15
+ "conversation",
16
+ "mailbox",
17
+ "sender",
18
+ "record",
14
19
  ];
15
20
  export function isTagKind(value) {
16
21
  return typeof value === "string" && TAG_KINDS.includes(value);
@@ -1,3 +1,3 @@
1
- export declare const OXYGEN_VERSION = "1.354.0";
1
+ export declare const OXYGEN_VERSION = "1.377.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.377.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:
@@ -1,11 +1,11 @@
1
- /** The three implemented Workspace Agent specialists, by stable slug. */
2
- export type WorkspaceAgentSlug = "inbox-reply-drafts" | "meeting-notetaker" | "knowledge-synthesis";
1
+ /** The implemented general Workspace Agent and specialists (incl. the auto-tagger), by stable slug. */
2
+ export type WorkspaceAgentSlug = "workspace" | "inbox-reply-drafts" | "meeting-notetaker" | "knowledge-synthesis" | "inbox-auto-tagger";
3
3
  /**
4
4
  * How the web layer builds a specialist's deep-link. A pure hint on the
5
5
  * descriptor (shared can't import the web URL helpers) that agent-registry
6
6
  * switches on to produce the actual `https://oxygen-agent.com/...` URL.
7
7
  */
8
- export type WorkspaceAgentDeepLinkKind = "inbox_reply_agent" | "notetaker" | "knowledge_agent";
8
+ export type WorkspaceAgentDeepLinkKind = "workspace_agent" | "inbox_reply_agent" | "notetaker" | "knowledge_agent" | "inbox_auto_tagger";
9
9
  /** A pure, serializable descriptor for one Workspace Agent specialist. */
10
10
  export type WorkspaceAgentDescriptor = {
11
11
  /** Stable identifier used across CLI/MCP/API. Never derived from the title. */
@@ -54,12 +54,13 @@ export type WorkspaceAgentRunItem = {
54
54
  webUrl: string | null;
55
55
  };
56
56
  /**
57
- * The roster. Exactly three entries — one per implemented specialist. Order is
58
- * stable (it drives the default CLI/widget ordering). Adding a fourth entry is a
59
- * deliberate product decision that requires the specialist to actually ship first.
57
+ * The roster. The general Workspace Agent plus the implemented specialists
58
+ * (five entries). Order is stable (it drives the default CLI/widget ordering).
59
+ * Adding an entry is a deliberate product decision that requires the worker to
60
+ * actually ship first.
60
61
  */
61
62
  export declare const WORKSPACE_AGENT_ROSTER: readonly WorkspaceAgentDescriptor[];
62
- /** True when `value` is one of the three implemented specialist slugs. */
63
+ /** True when `value` is one of the implemented specialist slugs. */
63
64
  export declare function isWorkspaceAgentSlug(value: unknown): value is WorkspaceAgentSlug;
64
65
  /** The descriptor for `slug`, or `undefined` when it is not a known specialist. */
65
66
  export declare function getWorkspaceAgentDescriptor(slug: string): WorkspaceAgentDescriptor | undefined;
@@ -1,11 +1,13 @@
1
1
  // The Agents primitive's roster (ADR 0011): the governed registry of OXYGEN's
2
- // built-in workspace agents. Still deliberately a *computed* registry — each
2
+ // built-in workspace agents. The general Workspace Agent is backed by the
3
+ // first-class ox_agents runtime; specialists remain computed from their owning
4
+ // primitive stores. Each
3
5
  // entry is a product-defined agent that ships as an approval-gated, observable
4
6
  // behavior elsewhere in the product, surfaced under /agents and `oxygen agent`.
5
7
  // Agents are governed workspace workers: the runtime envelope (approval posture,
6
8
  // credit budgets, run observability) is platform-enforced, never agent-optional.
7
- // User-defined custom agents are ratified (ADR 0011) but arrive with the unified
8
- // runtime envelope in a later phase — until then there is no create/define verb,
9
+ // User-defined custom agents are ratified (ADR 0011) but arrive after the unified
10
+ // Workspace Agent runtime — until then there is no custom create/delete verb,
9
11
  // and there are never placeholder rows: the roster lists only agents that are
10
12
  // actually implemented today. CRM-hygiene, deliverability, and any other roadmap
11
13
  // agent stay OUT until they ship — the no-placeholder-surface rule.
@@ -15,11 +17,24 @@
15
17
  // (config read/write, run history, deep-links) lives in the web layer's
16
18
  // agent-registry, which composes these descriptors with the existing helpers.
17
19
  /**
18
- * The roster. Exactly three entries — one per implemented specialist. Order is
19
- * stable (it drives the default CLI/widget ordering). Adding a fourth entry is a
20
- * deliberate product decision that requires the specialist to actually ship first.
20
+ * The roster. The general Workspace Agent plus the implemented specialists
21
+ * (five entries). Order is stable (it drives the default CLI/widget ordering).
22
+ * Adding an entry is a deliberate product decision that requires the worker to
23
+ * actually ship first.
21
24
  */
22
25
  export const WORKSPACE_AGENT_ROSTER = [
26
+ {
27
+ slug: "workspace",
28
+ title: "Workspace Agent",
29
+ subtitle: "Runs organization-grounded goals through OXYGEN's governed capability catalog.",
30
+ description: "Takes a workspace goal, plans the work, and operates tables, CRM, context, workflows, sequences, files, and integrations as one durable Agent run.",
31
+ owningPrimitive: "Agents",
32
+ configCommand: "oxygen agent set workspace",
33
+ configApiPath: "/api/cli/agent/workspace",
34
+ mcpConfigTools: ["oxygen_agent_update", "oxygen_agent_run"],
35
+ approvalPosture: "Uses a versioned standing authorization with optional allow, ask, and deny rules; platform previews, caps, deny rules, and run provenance always apply.",
36
+ deepLinkKind: "workspace_agent",
37
+ },
23
38
  {
24
39
  slug: "inbox-reply-drafts",
25
40
  title: "AI Sales Agent",
@@ -56,8 +71,20 @@ export const WORKSPACE_AGENT_ROSTER = [
56
71
  approvalPosture: "Writes working knowledge pages as logged revisions; canonical and pinned pages still require proposal approval.",
57
72
  deepLinkKind: "knowledge_agent",
58
73
  },
74
+ {
75
+ slug: "inbox-auto-tagger",
76
+ title: "AI Inbox Tagger",
77
+ subtitle: "Auto-tags new inbound conversations from your tag allowlist.",
78
+ description: "Classifies every new inbound email, LinkedIn, and WhatsApp conversation against your workspace tag allowlist and applies the matching tags automatically.",
79
+ owningPrimitive: "Messages",
80
+ configCommand: "oxygen inbox auto-tagger",
81
+ configApiPath: "/api/cli/inbox/auto-tagger",
82
+ mcpConfigTools: ["oxygen_inbox_auto_tagger_get", "oxygen_inbox_auto_tagger_set"],
83
+ approvalPosture: "Auto-applies internal workspace tags only — additive, never re-adds a tag you removed, and needs no approval queue because tags never leave the workspace.",
84
+ deepLinkKind: "inbox_auto_tagger",
85
+ },
59
86
  ];
60
- /** True when `value` is one of the three implemented specialist slugs. */
87
+ /** True when `value` is one of the implemented specialist slugs. */
61
88
  export function isWorkspaceAgentSlug(value) {
62
89
  return (typeof value === "string" &&
63
90
  WORKSPACE_AGENT_ROSTER.some((descriptor) => descriptor.slug === value));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.354.0",
3
+ "version": "1.377.3",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",
@@ -32,9 +32,12 @@
32
32
  "README.md"
33
33
  ],
34
34
  "dependencies": {
35
+ "@aws-sdk/client-s3": "3.1050.0",
36
+ "@aws-sdk/s3-request-presigner": "3.1050.0",
35
37
  "@opentelemetry/api": "1.9.1",
36
38
  "commander": "14.0.3",
37
39
  "esbuild": "0.27.7",
40
+ "langfuse": "3.38.20",
38
41
  "read-excel-file": "6.0.3",
39
42
  "typescript": "6.0.3",
40
43
  "@oxygen/shared": "0.0.0",