@oxygen-agent/cli 1.1014.1035 → 1.1014.1132

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 (77) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +1 -0
  3. package/dist/commands/columns.js +1 -1
  4. package/dist/commands/deliverability.js +2 -0
  5. package/dist/commands/sequences.js +30 -2
  6. package/dist/commands/viewers.js +1 -2
  7. package/dist/credentials.js +1 -1
  8. package/dist/customer-operation-commands.js +62 -3
  9. package/dist/help.js +1 -1
  10. package/dist/index.d.ts +0 -97
  11. package/dist/index.js +0 -98
  12. package/dist/workspace-placement-commands.d.ts +3 -0
  13. package/dist/workspace-placement-commands.js +42 -0
  14. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +6 -1
  15. package/node_modules/@oxygen/shared/dist/agent-sessions.js +18 -6
  16. package/node_modules/@oxygen/shared/dist/agents-enabled.d.ts +8 -9
  17. package/node_modules/@oxygen/shared/dist/agents-enabled.js +12 -8
  18. package/node_modules/@oxygen/shared/dist/capability-discovery.js +24 -6
  19. package/node_modules/@oxygen/shared/dist/column-autofill.js +2 -2
  20. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +1 -1
  21. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +1 -1
  22. package/node_modules/@oxygen/shared/dist/contracts/guided-activation.js +5 -5
  23. package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.d.ts +30 -0
  24. package/node_modules/@oxygen/shared/dist/contracts/guided-company-files.js +53 -0
  25. package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.d.ts +120 -0
  26. package/node_modules/@oxygen/shared/dist/contracts/guided-first-sourcing.js +48 -0
  27. package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.d.ts +267 -0
  28. package/node_modules/@oxygen/shared/dist/contracts/guided-onboarding-journey.js +63 -0
  29. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +6 -0
  30. package/node_modules/@oxygen/shared/dist/contracts/operations.js +9 -4
  31. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +2 -0
  32. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +3 -0
  33. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +9 -8
  34. package/node_modules/@oxygen/shared/dist/data-suppliers.d.ts +32 -2
  35. package/node_modules/@oxygen/shared/dist/data-suppliers.js +44 -4
  36. package/node_modules/@oxygen/shared/dist/email-tracking-token.d.ts +2 -0
  37. package/node_modules/@oxygen/shared/dist/email-tracking-token.js +1 -0
  38. package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +3 -4
  39. package/node_modules/@oxygen/shared/dist/feature-gates.js +3 -4
  40. package/node_modules/@oxygen/shared/dist/file-import.d.ts +46 -0
  41. package/node_modules/@oxygen/shared/dist/file-import.js +138 -0
  42. package/node_modules/@oxygen/shared/dist/guided-activation.d.ts +8 -0
  43. package/node_modules/@oxygen/shared/dist/guided-company-files.d.ts +13 -0
  44. package/node_modules/@oxygen/shared/dist/guided-company-files.js +14 -0
  45. package/node_modules/@oxygen/shared/dist/guided-first-sourcing.d.ts +116 -0
  46. package/node_modules/@oxygen/shared/dist/guided-first-sourcing.js +11 -0
  47. package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.d.ts +4 -0
  48. package/node_modules/@oxygen/shared/dist/guided-onboarding-answers.js +42 -0
  49. package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.d.ts +22 -0
  50. package/node_modules/@oxygen/shared/dist/guided-onboarding-company-context.js +38 -0
  51. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.d.ts +147 -0
  52. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.d.ts +12 -0
  53. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.fixture.js +83 -0
  54. package/node_modules/@oxygen/shared/dist/guided-onboarding-journey.js +44 -0
  55. package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.d.ts +7 -0
  56. package/node_modules/@oxygen/shared/dist/guided-onboarding-prospects.js +13 -0
  57. package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.d.ts +4 -0
  58. package/node_modules/@oxygen/shared/dist/guided-onboarding-signals.js +34 -0
  59. package/node_modules/@oxygen/shared/dist/guided-trial-billing.d.ts +26 -0
  60. package/node_modules/@oxygen/shared/dist/guided-trial-billing.js +19 -0
  61. package/node_modules/@oxygen/shared/dist/hosted-ai.js +108 -118
  62. package/node_modules/@oxygen/shared/dist/knowledge-links.d.ts +29 -0
  63. package/node_modules/@oxygen/shared/dist/knowledge-links.js +84 -1
  64. package/node_modules/@oxygen/shared/dist/linkedin-sequences.d.ts +5 -14
  65. package/node_modules/@oxygen/shared/dist/linkedin-sequences.js +22 -5
  66. package/node_modules/@oxygen/shared/dist/managed-scraper-backend.d.ts +18 -0
  67. package/node_modules/@oxygen/shared/dist/managed-scraper-backend.js +17 -0
  68. package/node_modules/@oxygen/shared/dist/teammates.d.ts +7 -5
  69. package/node_modules/@oxygen/shared/dist/teammates.js +11 -20
  70. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +88 -0
  71. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  72. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  73. package/node_modules/@oxygen/shared/dist/workspace-placement-view.d.ts +150 -0
  74. package/node_modules/@oxygen/shared/dist/workspace-placement-view.js +102 -0
  75. package/node_modules/@oxygen/shared/dist/workspace-placement.d.ts +84 -0
  76. package/node_modules/@oxygen/shared/dist/workspace-placement.js +107 -0
  77. package/package.json +1 -1
@@ -14,3 +14,32 @@ export type KnowledgeWikilink = {
14
14
  * empty key (e.g. [[---]]) are dropped.
15
15
  */
16
16
  export declare function extractKnowledgeLinks(body: string | null | undefined): KnowledgeWikilink[];
17
+ /**
18
+ * The exact vault paths, in priority order, and the basename that
19
+ * resolveKnowledgeVaultLink can match for a [[wikilink]] target written in the
20
+ * file at `currentPath`. A store fetches only these files and resolves against
21
+ * them with the same result as against the whole vault. Null when the target is
22
+ * empty or absolute.
23
+ */
24
+ export declare function knowledgeVaultLinkCandidates(target: string, currentPath: string): {
25
+ paths: string[];
26
+ basename: string | null;
27
+ } | null;
28
+ /**
29
+ * Resolve a [[wikilink]] target in a repository vault the way Obsidian does: a
30
+ * bare name prefers the same folder, then the vault-relative path, a path target
31
+ * is also tried relative to the current folder, and a bare name finally matches
32
+ * a basename that is unique in the vault. A target without an extension names a
33
+ * Markdown note. The web file view and the page-link graph both resolve through
34
+ * here, so a link that opens a note is the edge the graph shows.
35
+ */
36
+ export declare function resolveKnowledgeVaultLink<T extends {
37
+ path: string;
38
+ }>(target: string, currentPath: string, files: readonly T[]): T | null;
39
+ /**
40
+ * Every link key a [[wikilink]] that resolves to the vault file at `path` can
41
+ * carry: each trailing path suffix, and for a note also without its .md
42
+ * extension. A new or moved file can satisfy only dangling links with one of
43
+ * these keys.
44
+ */
45
+ export declare function knowledgeVaultLinkKeys(path: string): string[];
@@ -4,7 +4,8 @@
4
4
  // here and nowhere else. The regex + alias/heading stripping mirror the vault
5
5
  // component's original parser (apps/web/src/components/vault/vault.ts), which
6
6
  // now delegates here — [[Target|alias]] and [[Target#heading]] both link to
7
- // Target.
7
+ // Target. Links inside a connected repository resolve by vault path first
8
+ // (resolveKnowledgeVaultLink), for the mounted file view and the graph alike.
8
9
  import { knowledgeLinkKey } from "./knowledge-constants.js";
9
10
  const WIKILINK_RE = /\[\[([^\]]+)\]\]/g;
10
11
  /**
@@ -36,3 +37,85 @@ export function extractKnowledgeLinks(body) {
36
37
  }
37
38
  return [...byKey.values()];
38
39
  }
40
+ function normalizedVaultPath(candidate) {
41
+ const normalized = [];
42
+ for (const segment of candidate.split("/")) {
43
+ if (!segment || segment === ".")
44
+ continue;
45
+ if (segment === "..") {
46
+ if (!normalized.pop())
47
+ return null;
48
+ }
49
+ else {
50
+ normalized.push(segment);
51
+ }
52
+ }
53
+ return normalized.join("/") || null;
54
+ }
55
+ /**
56
+ * The exact vault paths, in priority order, and the basename that
57
+ * resolveKnowledgeVaultLink can match for a [[wikilink]] target written in the
58
+ * file at `currentPath`. A store fetches only these files and resolves against
59
+ * them with the same result as against the whole vault. Null when the target is
60
+ * empty or absolute.
61
+ */
62
+ export function knowledgeVaultLinkCandidates(target, currentPath) {
63
+ const pathTarget = target.split("#", 1)[0]?.trim();
64
+ if (!pathTarget || pathTarget.startsWith("/"))
65
+ return null;
66
+ const withExtension = /\.[^/]+$/.test(pathTarget) ? pathTarget : `${pathTarget}.md`;
67
+ const directory = currentPath.includes("/") ? currentPath.slice(0, currentPath.lastIndexOf("/")) : "";
68
+ const paths = [];
69
+ if (!pathTarget.includes("/"))
70
+ paths.push(`${directory ? `${directory}/` : ""}${withExtension}`);
71
+ const vaultPath = normalizedVaultPath(withExtension);
72
+ if (vaultPath)
73
+ paths.push(vaultPath);
74
+ if (pathTarget.includes("/")) {
75
+ const relativePath = normalizedVaultPath(`${directory}/${withExtension}`);
76
+ if (relativePath)
77
+ paths.push(relativePath);
78
+ }
79
+ return { paths, basename: pathTarget.includes("/") ? null : withExtension };
80
+ }
81
+ /**
82
+ * Resolve a [[wikilink]] target in a repository vault the way Obsidian does: a
83
+ * bare name prefers the same folder, then the vault-relative path, a path target
84
+ * is also tried relative to the current folder, and a bare name finally matches
85
+ * a basename that is unique in the vault. A target without an extension names a
86
+ * Markdown note. The web file view and the page-link graph both resolve through
87
+ * here, so a link that opens a note is the edge the graph shows.
88
+ */
89
+ export function resolveKnowledgeVaultLink(target, currentPath, files) {
90
+ const candidates = knowledgeVaultLinkCandidates(target, currentPath);
91
+ if (!candidates)
92
+ return null;
93
+ for (const path of candidates.paths) {
94
+ const file = files.find((candidate) => candidate.path === path);
95
+ if (file)
96
+ return file;
97
+ }
98
+ if (!candidates.basename)
99
+ return null;
100
+ const matches = files.filter((file) => file.path.split("/").at(-1) === candidates.basename);
101
+ return matches.length === 1 ? matches[0] : null;
102
+ }
103
+ /**
104
+ * Every link key a [[wikilink]] that resolves to the vault file at `path` can
105
+ * carry: each trailing path suffix, and for a note also without its .md
106
+ * extension. A new or moved file can satisfy only dangling links with one of
107
+ * these keys.
108
+ */
109
+ export function knowledgeVaultLinkKeys(path) {
110
+ const segments = path.split("/");
111
+ const keys = new Set();
112
+ for (let start = segments.length - 1; start >= 0; start -= 1) {
113
+ const suffix = segments.slice(start).join("/");
114
+ for (const form of /\.md$/.test(suffix) ? [suffix, suffix.slice(0, -3)] : [suffix]) {
115
+ const key = knowledgeLinkKey(form);
116
+ if (key)
117
+ keys.add(key);
118
+ }
119
+ }
120
+ return [...keys];
121
+ }
@@ -202,17 +202,6 @@ export type LinkedInRefusalScope =
202
202
  | "already_engaged"
203
203
  /** Unrecognised. Callers MUST preserve their existing behaviour. */
204
204
  | "unknown";
205
- /**
206
- * Classify a LinkedIn refusal from the provider's own account of it.
207
- *
208
- * Deliberately a CLOSED set of anchored, full-string matches, in the same
209
- * discipline as the sequence-recovery allowlists: matching vendor prose is
210
- * fragile, so an unrecognised message must degrade to `unknown` and leave the
211
- * caller's behaviour exactly as it was. Extending the set is an evidence-driven
212
- * act — callers log what they could not classify.
213
- *
214
- * Pure and total; never throws.
215
- */
216
205
  export declare function classifyLinkedInRefusal(input: {
217
206
  providerType: string | null;
218
207
  providerMessage: string | null;
@@ -394,9 +383,11 @@ export declare function linkedInLimitCautionNotices(metadata: Record<string, unk
394
383
  export declare const LINKEDIN_REINVITE_COOLDOWN_DAYS = 21;
395
384
  /**
396
385
  * Whether a refused invitation is LinkedIn's re-invite cooldown: Unipile answers
397
- * HTTP 422 with the `cannot_resend_yet` error type. Anchored on the type, with
398
- * the message as a fallback for the provider shapes that carry the type only in
399
- * the sentence. Pure and total.
386
+ * HTTP 422 with the `cannot_resend_yet` error type, or with
387
+ * `provider/unprocessable_entity` and LinkedIn's "already handled a request from
388
+ * you in the past 3 weeks" sentence. Anchored on the type, with the message as a
389
+ * fallback for the provider shapes that carry the type only in the sentence.
390
+ * Pure and total.
400
391
  */
401
392
  export declare function isLinkedInReinviteCooldownRefusal(input: {
402
393
  status: number | null;
@@ -113,6 +113,14 @@ export const LINKEDIN_INVITE_NOTE_LIMIT_BASIC = 200;
113
113
  *
114
114
  * Pure and total; never throws.
115
115
  */
116
+ /**
117
+ * LinkedIn's sentence when an invitation is refused because the member handled
118
+ * an earlier one from this account (accepted, ignored or withdrawn) within three
119
+ * weeks. Unipile returns it as HTTP 422 `provider/unprocessable_entity`, the v2
120
+ * client prefixes the title. Verified on dev 2026-10-17 (Unipile req-3i5d,
121
+ * req-7edn, req-7nsa): no invitation was created.
122
+ */
123
+ const LINKEDIN_RECENTLY_HANDLED_INVITE_MESSAGE = /^(?:Unprocessable entity: )?This user has already handled a request from you in the past 3 weeks\./u;
116
124
  export function classifyLinkedInRefusal(input) {
117
125
  const message = typeof input.providerMessage === "string" ? input.providerMessage.trim() : "";
118
126
  if (message.length === 0)
@@ -146,7 +154,7 @@ export function classifyLinkedInRefusal(input) {
146
154
  }
147
155
  if (input.status === 422
148
156
  && input.providerType === "provider/unprocessable_entity"
149
- && /^Unprocessable entity: This user has already handled a request from you in the past 3 weeks\./u.test(message)) {
157
+ && LINKEDIN_RECENTLY_HANDLED_INVITE_MESSAGE.test(message)) {
150
158
  return "already_engaged";
151
159
  }
152
160
  if (input.status === 409
@@ -440,14 +448,23 @@ export function linkedInLimitCautionNotices(metadata, overrides) {
440
448
  export const LINKEDIN_REINVITE_COOLDOWN_DAYS = 21;
441
449
  /**
442
450
  * Whether a refused invitation is LinkedIn's re-invite cooldown: Unipile answers
443
- * HTTP 422 with the `cannot_resend_yet` error type. Anchored on the type, with
444
- * the message as a fallback for the provider shapes that carry the type only in
445
- * the sentence. Pure and total.
451
+ * HTTP 422 with the `cannot_resend_yet` error type, or with
452
+ * `provider/unprocessable_entity` and LinkedIn's "already handled a request from
453
+ * you in the past 3 weeks" sentence. Anchored on the type, with the message as a
454
+ * fallback for the provider shapes that carry the type only in the sentence.
455
+ * Pure and total.
446
456
  */
447
457
  export function isLinkedInReinviteCooldownRefusal(input) {
448
458
  if (input.status !== 422)
449
459
  return false;
450
460
  if (typeof input.providerType === "string" && /(?:^|\/)cannot_resend_yet$/u.test(input.providerType.trim()))
451
461
  return true;
452
- return typeof input.providerMessage === "string" && /\bcannot_resend_yet\b/u.test(input.providerMessage);
462
+ if (typeof input.providerMessage !== "string")
463
+ return false;
464
+ if (/\bcannot_resend_yet\b/u.test(input.providerMessage))
465
+ return true;
466
+ // The same refusal in LinkedIn's own words. Without it, the BullMQ runtime
467
+ // read the write as possibly delivered and parked the lead for reconciliation.
468
+ return input.providerType === "provider/unprocessable_entity"
469
+ && LINKEDIN_RECENTLY_HANDLED_INVITE_MESSAGE.test(input.providerMessage.trim());
453
470
  }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Which vendor serves OXYGEN's managed `scraper.*` LinkedIn rail
3
+ * (LINKEDIN_SCRAPER_PROVIDER, Philipp 2026-10-17: a pure provider exchange from
4
+ * Up2Data to HarvestAPI behind a Doppler switch). Up2Data is the default, so an
5
+ * unset or unrecognised value never moves paid traffic.
6
+ *
7
+ * Dependency-free and browser-safe: a bundle without the server environment
8
+ * reads the default, so a label rendered in the browser must not rely on it.
9
+ */
10
+ export type ManagedScraperBackend = "up2data" | "harvestapi";
11
+ export declare const MANAGED_SCRAPER_BACKEND_ENV = "LINKEDIN_SCRAPER_PROVIDER";
12
+ type Env = Readonly<Record<string, string | undefined>>;
13
+ /** The server environment, or null in a bundle that has none. */
14
+ export declare function serverEnv(): Env | null;
15
+ /** The configured backend, or null when the value names neither vendor. */
16
+ export declare function parseManagedScraperBackend(value: string | undefined): ManagedScraperBackend | null;
17
+ export declare function readManagedScraperBackend(env?: Env | null): ManagedScraperBackend;
18
+ export {};
@@ -0,0 +1,17 @@
1
+ export const MANAGED_SCRAPER_BACKEND_ENV = "LINKEDIN_SCRAPER_PROVIDER";
2
+ /** The server environment, or null in a bundle that has none. */
3
+ export function serverEnv() {
4
+ const env = typeof process !== "undefined" ? process.env : undefined;
5
+ // `window` is absent from this package's types (no DOM lib), so read it off globalThis.
6
+ return env && !("window" in globalThis) ? env : null;
7
+ }
8
+ /** The configured backend, or null when the value names neither vendor. */
9
+ export function parseManagedScraperBackend(value) {
10
+ const normalized = value?.trim().toLowerCase();
11
+ if (!normalized || normalized === "up2data")
12
+ return "up2data";
13
+ return normalized === "harvestapi" ? "harvestapi" : null;
14
+ }
15
+ export function readManagedScraperBackend(env = serverEnv()) {
16
+ return parseManagedScraperBackend(env?.[MANAGED_SCRAPER_BACKEND_ENV]) ?? "up2data";
17
+ }
@@ -3,12 +3,14 @@
3
3
  * per Workspace Oxygent, on the Copilot runtime. Explicit Personal Oxygents
4
4
  * are creator-only; the first one stays Workspace (Philipp, 2026-10-16).
5
5
  *
6
- * Staff only: on dev, and in production's staff workspaces, the ones Agents run
7
- * in (`agentsEnabledIn`; Tim, 2026-10-16). `OXYGEN_TEAMMATES_ENABLED` is the
8
- * switch; when it is unset the feature follows the Ongoing Agent sessions staff
9
- * gate, because teammates re-scope those sessions into routines. Shadow never
10
- * qualifies.
6
+ * Who has them: `OXYGEN_TEAMMATES_ENABLED` is the switch, "staff" for staff emails
7
+ * and "all" for every member who is not a client login (the customer launch,
8
+ * Tim, 2026-10-17). Unset, it follows the Ongoing Agent sessions switch, because
9
+ * teammates re-scope those sessions into routines. Only workspaces Agents run in
10
+ * qualify (`agentsEnabledIn`); shadow never does.
11
11
  */
12
+ export type TeammatesAudience = "off" | "staff" | "all";
13
+ export declare function teammatesAudience(env?: Record<string, string | undefined>): TeammatesAudience;
12
14
  export declare function teammatesEnabled(env?: Record<string, string | undefined>): boolean;
13
15
  /** Whether this workspace has Oxygents. */
14
16
  export declare function teammatesEnabledIn(organizationId: string | null | undefined, env?: Record<string, string | undefined>): boolean;
@@ -1,34 +1,25 @@
1
1
  import { agentsEnabled, agentsEnabledIn } from "./agents-enabled.js";
2
2
  import { guidedActivationEnabled } from "./guided-activation.js";
3
3
  import { isStaffEmail } from "./staff.js";
4
- /**
5
- * AI teammates (ADR 0032): Agents people talk to in one ongoing shared chat
6
- * per Workspace Oxygent, on the Copilot runtime. Explicit Personal Oxygents
7
- * are creator-only; the first one stays Workspace (Philipp, 2026-10-16).
8
- *
9
- * Staff only: on dev, and in production's staff workspaces, the ones Agents run
10
- * in (`agentsEnabledIn`; Tim, 2026-10-16). `OXYGEN_TEAMMATES_ENABLED` is the
11
- * switch; when it is unset the feature follows the Ongoing Agent sessions staff
12
- * gate, because teammates re-scope those sessions into routines. Shadow never
13
- * qualifies.
14
- */
15
- export function teammatesEnabled(env = process.env) {
4
+ export function teammatesAudience(env = process.env) {
16
5
  if (env.OXYGEN_DEPLOY_ENV !== "selfhost-dev" && env.OXYGEN_DEPLOY_ENV !== "selfhost-prod")
17
- return false;
6
+ return "off";
18
7
  if (!agentsEnabled(env))
19
- return false;
20
- const flag = env.OXYGEN_TEAMMATES_ENABLED?.trim().toLowerCase();
21
- if (flag)
22
- return flag === "staff";
23
- return env.OXYGEN_AGENT_SESSIONS_ENABLED === "staff";
8
+ return "off";
9
+ const flag = env.OXYGEN_TEAMMATES_ENABLED?.trim().toLowerCase() || env.OXYGEN_AGENT_SESSIONS_ENABLED?.trim();
10
+ return flag === "staff" || flag === "all" ? flag : "off";
11
+ }
12
+ export function teammatesEnabled(env = process.env) {
13
+ return teammatesAudience(env) !== "off";
24
14
  }
25
15
  /** Whether this workspace has Oxygents. */
26
16
  export function teammatesEnabledIn(organizationId, env = process.env) {
27
17
  return teammatesEnabled(env) && agentsEnabledIn(organizationId, env);
28
18
  }
29
19
  export function teammatesAvailableTo(env, viewer) {
30
- return teammatesEnabledIn(viewer.organizationId, env) && viewer.member && viewer.oxygenRole !== "client"
31
- && (guidedActivationEnabled(env) || isStaffEmail(viewer.email, env));
20
+ if (!teammatesEnabledIn(viewer.organizationId, env) || !viewer.member || viewer.oxygenRole === "client")
21
+ return false;
22
+ return teammatesAudience(env) === "all" || guidedActivationEnabled(env) || isStaffEmail(viewer.email, env);
32
23
  }
33
24
  /**
34
25
  * The Workspace Agent's slug. Every workspace's first Oxygent is that Agent: an
@@ -14,6 +14,22 @@ export function capabilityDiscoveryRecovery(query, surface) {
14
14
  // connection routes.
15
15
  export function inferUserCapabilityRoute(query) {
16
16
  const normalized = query.toLowerCase().replace(/[_-]+/g, " ");
17
+ const journeyAction = savedOnboardingJourneyAction(normalized);
18
+ if (journeyAction) {
19
+ const route = getCapabilityRouteMatch("agents");
20
+ if (route) {
21
+ const recommendedTools = [`oxygen_onboarding_journey_${journeyAction}`, ...(journeyAction === "get" ? [] : ["oxygen_onboarding_journey_get"])];
22
+ const recommendedCommands = [`onboarding journey ${journeyAction}`, ...(journeyAction === "get" ? [] : ["onboarding journey get"])];
23
+ return { ...route, recommendedTools, recommendedCommands, card: { ...route.card,
24
+ owns: "Creator-private saved guided onboarding answers, current step and pause/resume position.",
25
+ notFor: "Human workspace invitations or permissions, destructive workspace reset, paid Oxygent Start or routine execution.",
26
+ gatewayTools: recommendedTools, gatewayCommands: recommendedCommands,
27
+ posture: journeyAction === "get" ? "read_only" : "workspace_write",
28
+ execution: journeyAction === "get" ? "Read saved onboarding and its current native step. This read starts no research, model, billing reconciliation or message send."
29
+ : "Read activationId and current revision first, then hydrate the selected native operation. Use a stable mutation_id; Back and non-destructive restart preserve resources and funding. Company answers prepare native sources and existing bounded research consent. Discovery executes nothing and supplies no approval or billing authority.",
30
+ } };
31
+ }
32
+ }
17
33
  // Exact native session activation must outrank the generic Agent gateways.
18
34
  // Discovery supplies no approval, preview token or invented session binding.
19
35
  if (/\bagent session activate\b/.test(normalized)
@@ -61,6 +77,23 @@ export function inferUserCapabilityRoute(query) {
61
77
  recommendedCommands: ["context profile get", "knowledge bootstrap status", "onboarding activation status"],
62
78
  exactCommand: "oxygen context profile get --json", exactMcpCall: { tool: "oxygen_context_profile_get", arguments: {} } };
63
79
  }
80
+ // Explicit guided-journey discovery precedes the individual campaign,
81
+ // review and routine nouns that describe its later steps. Consulting keeps
82
+ // the ordinary onboarding card; this recommendation advertises DEV only.
83
+ if (isGuidedOnboardingDiscoveryIntent(normalized)) {
84
+ const route = getCapabilityRouteMatch("onboarding-and-copilot");
85
+ if (route) {
86
+ const recommendedTools = ["oxygen_onboarding_activation_status", "oxygen_onboarding_journey_get"];
87
+ const recommendedCommands = ["onboarding activation status", "onboarding journey get"];
88
+ return { ...route, recommendedTools, recommendedCommands, card: { ...route.card,
89
+ owns: "Creator-owned DEV guided onboarding answers, native resources, current step and saved navigation.",
90
+ notFor: "Production onboarding execution, human membership or ordinary GTM consulting.",
91
+ posture: "read_only", gatewayTools: recommendedTools, gatewayCommands: recommendedCommands,
92
+ execution: "DEV-only two-part guided onboarding; unavailable in production, shadow, local or production-data workspaces. Read your activation first, then use its activationId to read the saved journey and revision. If absent, inspect the free activation-start schema and choose own_agent or oxygent. Resume the native step; answers, prospects, TEST billing, campaigns and routines retain their own schemas, eligibility and approvals. These reads start no research, checkout, model, schedule or send. Ordinary GTM consulting remains with Recipes/Copilot.",
93
+ }, exactCommand: "oxygen onboarding activation status --json",
94
+ exactMcpCall: { tool: "oxygen_onboarding_activation_status", arguments: {} } };
95
+ }
96
+ }
64
97
  if ((/\b(?:linkedin|sequence|campaign)\b/.test(normalized) && /\b(?:preview|review|readiness)\b/.test(normalized)
65
98
  && /\b(?:launch|start)\b/.test(normalized) && /\b(?:approval|send limits?|caps?)\b/.test(normalized))
66
99
  || (isLinkedInCampaignIntent(normalized) && /\breadiness\b/.test(normalized) && /\bpreview\b/.test(normalized))) {
@@ -159,6 +192,45 @@ export function inferUserCapabilityRoute(query) {
159
192
  // intent is exactly what this wrapper exists to answer.
160
193
  return getCapabilityRouteMatch("connected-integrations") ?? route;
161
194
  }
195
+ /** Saved progress is distinct from general signup research, attended Start,
196
+ * routine schedules and human membership. Pause wins over a simultaneous resume. */
197
+ function savedOnboardingJourneyAction(query) {
198
+ if (!/\bonboarding\b/.test(query) || !/\b(?:journey|guided|answers?|steps?|tour)\b/.test(query)
199
+ || /\b(?:invite|members?|membership|permissions?|roles?|human|colleague|coworker|delete|destroy|wipe)\b/.test(query))
200
+ return null;
201
+ if (/\b(?:pause|paused|navigation)\b/.test(query) || (/\btour\b/.test(query) && /\bresume\b/.test(query)))
202
+ return "navigation";
203
+ if (/\brestart\b/.test(query))
204
+ return "restart";
205
+ if (/\bback\b/.test(query))
206
+ return "back";
207
+ if (/\bsave\b/.test(query))
208
+ return "save";
209
+ if (/\b(?:resume|read|get)\b/.test(query))
210
+ return "get";
211
+ return null;
212
+ }
213
+ /** A saved action is handled first. This is discovery of the multi-step
214
+ * product journey, not generic advice about improving a GTM operation. */
215
+ function isGuidedOnboardingDiscoveryIntent(query) {
216
+ if (!/\bonboarding\b/.test(query)
217
+ || /\b(?:invite|members?|membership|permissions?|roles?|human|colleague|coworker|delete|destroy|wipe|paid)\b/.test(query))
218
+ return false;
219
+ if (/^(?:guided )?onboarding$/.test(query.trim()))
220
+ return true;
221
+ return /\b(?:first sourcing|daily rhythm)\b/.test(query)
222
+ || (/\b(?:guide|guided|journey|part [12])\b/.test(query)
223
+ && /\b(?:business|company|icp|personas?|prospects?|trial|campaign|sequence|briefing)\b/.test(query))
224
+ || (/\b(?:trial|prospects?|personas?)\b/.test(query)
225
+ && /\b(?:context|icp|campaign|sequence)\b/.test(query))
226
+ // Company-statement review is a guided step even without the words guide
227
+ // or journey. Existing channel conversations keep their message owner.
228
+ || (/\bcompany profile\b/.test(query)
229
+ && /\b(?:review|correct|confirm)\b/.test(query)
230
+ && /\b(?:statement|profile revision)\b/.test(query)
231
+ && /\b(?:prospects?|evidence|sources?)\b/.test(query)
232
+ && !/\b(?:inbox|unibox|messages?|threads?|repl(?:y|ies)|email|whatsapp|slack|teams)\b/.test(query));
233
+ }
162
234
  function isCompanyOnboardingContextIntent(query) {
163
235
  return /\bonboard\w*\b/.test(query)
164
236
  && /\bcompany\b/.test(query)
@@ -266,6 +338,22 @@ function oxygentsRoute(normalized) {
266
338
  const group = /\bgroups?\b/.test(normalized);
267
339
  const verb = (group ? OXYGENT_GROUP_VERBS : OXYGENT_VERBS).find(([, words]) => words.test(normalized))?.[0]
268
340
  ?? (group ? "groups" : "list");
341
+ // Visibility discovery asks about the contract, not existing conversations.
342
+ // Keep the shared router's create/update/access recommendations even when
343
+ // the product noun would otherwise select the roster or "history" GET.
344
+ // Explicit content reads and other operations retain their native verbs.
345
+ const privacy = /\b(?:visibility|personal|workspace|private|shared)\b/.test(normalized);
346
+ const rosterRead = /\b(?:list|inventory|roster)\b|\bwhich\b.*\b(?:have|available)\b/.test(normalized);
347
+ // Bare history asks read content; creation/configuration asks describe its contract.
348
+ const historyContract = /\b(?:visibility|configur\w*|settings?|privacy|defaults?|create|creating|make|add|new|set up|spawn|hire)\b/.test(normalized);
349
+ const contentRead = /\b(?:read|see|view|show|transcript)\b/.test(normalized)
350
+ || (/\bhistory\b/.test(normalized) && !historyContract);
351
+ const contentSend = /\b(?:messages?|ask|tell|send|talk|continue|resume|handoff|delegat\w*)\b|\b(?:chat with|hand\w* off)\b/.test(normalized);
352
+ if (!group && privacy && ((verb === "list" && !rosterRead) || verb === "create"
353
+ || (verb === "update" && /\bvisibility\b/.test(normalized))
354
+ || (verb === "get" && !contentRead) || (verb === "message" && !contentSend))) {
355
+ return inferCapabilityRoute(normalized);
356
+ }
269
357
  // Sharing is read and changed as a pair: who has access, then share.
270
358
  const pair = verb === "share" ? ["access"] : verb === "access" ? ["share"] : [];
271
359
  // A group's id comes from its list; the roster reads follow every ask.
@@ -1 +1 @@
1
- export declare const OXYGEN_BUILD_VERSION = "1.1014.1035";
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1014.1132";
@@ -1,2 +1,2 @@
1
1
  // GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
2
- export const OXYGEN_BUILD_VERSION = "1.1014.1035";
2
+ export const OXYGEN_BUILD_VERSION = "1.1014.1132";
@@ -0,0 +1,150 @@
1
+ import { type WorkspacePlacementPlan, type WorkspacePlacementObservation } from "./workspace-placement.js";
2
+ export type WorkspacePlacementView = {
3
+ id: string;
4
+ provider: "internal";
5
+ billing_mode: "internal";
6
+ credits_used: 0;
7
+ status: "running" | "completed" | "incomplete";
8
+ created_at: string;
9
+ message_count: number;
10
+ variants: Array<{
11
+ key: string;
12
+ planned: number;
13
+ observed: number;
14
+ google_primary: number;
15
+ google_promotions: number;
16
+ google_social: number;
17
+ google_updates: number;
18
+ google_forums: number;
19
+ microsoft_focused: number;
20
+ microsoft_other: number;
21
+ spam: number;
22
+ other_folder: number;
23
+ unknown_folder: number;
24
+ unknown_category: number;
25
+ not_observed: number;
26
+ failed: number;
27
+ send_unknown: number;
28
+ read_blocked: number;
29
+ }>;
30
+ probes: Array<{
31
+ id: string;
32
+ sender: string;
33
+ receiver: string;
34
+ receiver_provider: string;
35
+ variant: string;
36
+ subject: string;
37
+ sender_name: string | null;
38
+ body_text_characters: number;
39
+ body_html_characters: number;
40
+ state: string;
41
+ status_label: string;
42
+ delivery?: {
43
+ id: string;
44
+ status: string;
45
+ sent_at: string | null;
46
+ provider_message_id: string | null;
47
+ };
48
+ observation?: WorkspacePlacementObservation;
49
+ }>;
50
+ has_more?: boolean;
51
+ next_cursor?: string | null;
52
+ probe_count?: number;
53
+ plan_hash: string;
54
+ web_url: string;
55
+ directional_note: string;
56
+ };
57
+ export declare function workspacePlacementView(input: {
58
+ id: string;
59
+ plan: WorkspacePlacementPlan;
60
+ outcomes: Record<string, {
61
+ state: string;
62
+ observation?: WorkspacePlacementObservation;
63
+ }>;
64
+ deliveries?: Record<string, {
65
+ id: string;
66
+ status: string;
67
+ sent_at: string | null;
68
+ provider_message_id: string | null;
69
+ }>;
70
+ created_at: Date | string;
71
+ }, webUrl: string): WorkspacePlacementView;
72
+ export declare function workspacePlacementStateLabel(state: string): string;
73
+ export type WorkspacePlacementCopyPart = "body_text" | "body_html";
74
+ export type WorkspacePlacementCopyPage = {
75
+ id: string;
76
+ variant: string;
77
+ sender: string;
78
+ sender_name: string | null;
79
+ receiver: string;
80
+ subject: string;
81
+ part: WorkspacePlacementCopyPart;
82
+ offset: number;
83
+ total_characters: number;
84
+ text: string;
85
+ has_more: boolean;
86
+ next_offset: number | null;
87
+ };
88
+ export declare function workspacePlacementProbeMeta(probe: WorkspacePlacementPlan["probes"][number]): {
89
+ id: string;
90
+ sender: string;
91
+ sender_name: string | null;
92
+ receiver: string;
93
+ receiver_provider: "google" | "microsoft";
94
+ variant: string;
95
+ subject: string;
96
+ body_text_characters: number;
97
+ body_html_characters: number;
98
+ };
99
+ export declare function workspacePlacementCopyPage(plan: WorkspacePlacementPlan, input: {
100
+ probeId: unknown;
101
+ part?: unknown;
102
+ offset?: unknown;
103
+ limit?: unknown;
104
+ }): WorkspacePlacementCopyPage;
105
+ export declare function workspacePlacementProbePage<T extends {
106
+ id: string;
107
+ }>(probes: T[], input: {
108
+ cursor?: unknown;
109
+ limit?: unknown;
110
+ }, defaultLimit?: number): {
111
+ probes: T[];
112
+ probe_count: number;
113
+ has_more: boolean;
114
+ next_cursor: string | null;
115
+ };
116
+ export declare function workspacePlacementPreviewPage(plan: WorkspacePlacementPlan, input?: {
117
+ cursor?: unknown;
118
+ limit?: unknown;
119
+ }): {
120
+ request_id: string;
121
+ organization_id: string;
122
+ provider: "internal";
123
+ billing_mode: "internal";
124
+ credits: 0;
125
+ allow_paused_probe: boolean;
126
+ receiver_scope?: "workspace" | "network";
127
+ rendered_at?: string;
128
+ rendering?: {
129
+ tracking_opens: boolean;
130
+ tracking_clicks: boolean;
131
+ tracking_host: string | null;
132
+ };
133
+ excluded_self_sends: number;
134
+ message_count: number;
135
+ plan_hash: string;
136
+ probes: {
137
+ id: string;
138
+ sender: string;
139
+ sender_name: string | null;
140
+ receiver: string;
141
+ receiver_provider: "google" | "microsoft";
142
+ variant: string;
143
+ subject: string;
144
+ body_text_characters: number;
145
+ body_html_characters: number;
146
+ }[];
147
+ probe_count: number;
148
+ has_more: boolean;
149
+ next_cursor: string | null;
150
+ };