@oxygen-agent/cli 1.853.1 → 1.861.0
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.
- package/README.md +1 -1
- package/dist/cli-values.d.ts +16 -0
- package/dist/cli-values.js +31 -0
- package/dist/command-manifest.js +2 -0
- package/dist/index.js +257 -21
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +7 -9
- package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +1 -8
- package/node_modules/@oxygen/shared/dist/copilot-journeys.js +6 -13
- package/node_modules/@oxygen/shared/dist/sending-seats.d.ts +28 -0
- package/node_modules/@oxygen/shared/dist/sending-seats.js +43 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
package/dist/cli-values.d.ts
CHANGED
|
@@ -2,6 +2,22 @@
|
|
|
2
2
|
export declare function readPositiveInt(value: string | undefined): number | undefined;
|
|
3
3
|
/** Read a string field from an unknown record-shaped value, or null when it is missing/non-string. */
|
|
4
4
|
export declare function readRecordString(value: unknown, key: string): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Parse repeated `key=value` flags into an object. Call this INSIDE an action,
|
|
7
|
+
* never as a Commander `parseArg`.
|
|
8
|
+
*
|
|
9
|
+
* That placement is the whole point. A parser passed to Commander runs during
|
|
10
|
+
* `parseOptions`, OUTSIDE the action's try/catch, so throwing there escapes as
|
|
11
|
+
* an uncaught exception: a raw Node stack trace instead of the JSON envelope,
|
|
12
|
+
* and exit code 0 — a failure reporting success. Collect with the total
|
|
13
|
+
* `collectRepeatable`, then validate here where handleAsyncAction can shape the
|
|
14
|
+
* error. (Same shape as index.ts's readNamedCredentials, which predates this.)
|
|
15
|
+
*
|
|
16
|
+
* Later occurrences of the same key win, which is what a user retyping one
|
|
17
|
+
* binding expects. Only the first `=` splits, so a value containing `=`
|
|
18
|
+
* survives intact.
|
|
19
|
+
*/
|
|
20
|
+
export declare function parseKeyValuePairs(values: readonly string[], example?: string): Record<string, string>;
|
|
5
21
|
/** Parse a JSON string that must decode to a plain object, throwing a typed invalid_json error otherwise. */
|
|
6
22
|
export declare function parseJsonObject(value: string): Record<string, unknown>;
|
|
7
23
|
/** Read a JSON-object CLI option (e.g. `--selection-json`, `--filter-tree-json`), or undefined when absent. */
|
package/dist/cli-values.js
CHANGED
|
@@ -27,6 +27,37 @@ export function readRecordString(value, key) {
|
|
|
27
27
|
const entry = value[key];
|
|
28
28
|
return typeof entry === "string" ? entry : null;
|
|
29
29
|
}
|
|
30
|
+
/**
|
|
31
|
+
* Parse repeated `key=value` flags into an object. Call this INSIDE an action,
|
|
32
|
+
* never as a Commander `parseArg`.
|
|
33
|
+
*
|
|
34
|
+
* That placement is the whole point. A parser passed to Commander runs during
|
|
35
|
+
* `parseOptions`, OUTSIDE the action's try/catch, so throwing there escapes as
|
|
36
|
+
* an uncaught exception: a raw Node stack trace instead of the JSON envelope,
|
|
37
|
+
* and exit code 0 — a failure reporting success. Collect with the total
|
|
38
|
+
* `collectRepeatable`, then validate here where handleAsyncAction can shape the
|
|
39
|
+
* error. (Same shape as index.ts's readNamedCredentials, which predates this.)
|
|
40
|
+
*
|
|
41
|
+
* Later occurrences of the same key win, which is what a user retyping one
|
|
42
|
+
* binding expects. Only the first `=` splits, so a value containing `=`
|
|
43
|
+
* survives intact.
|
|
44
|
+
*/
|
|
45
|
+
export function parseKeyValuePairs(values, example = "url=linkedin_url") {
|
|
46
|
+
const out = {};
|
|
47
|
+
for (const value of values) {
|
|
48
|
+
const separator = value.indexOf("=");
|
|
49
|
+
const key = separator > 0 ? value.slice(0, separator).trim() : "";
|
|
50
|
+
const entry = separator > 0 ? value.slice(separator + 1).trim() : "";
|
|
51
|
+
if (!key || !entry) {
|
|
52
|
+
throw new OxygenError("invalid_request", `Expected key=value, got "${value}".`, {
|
|
53
|
+
details: { value, example },
|
|
54
|
+
exitCode: 1,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
out[key] = entry;
|
|
58
|
+
}
|
|
59
|
+
return out;
|
|
60
|
+
}
|
|
30
61
|
/** Parse a JSON string that must decode to a plain object, throwing a typed invalid_json error otherwise. */
|
|
31
62
|
export function parseJsonObject(value) {
|
|
32
63
|
let parsed;
|
package/dist/command-manifest.js
CHANGED
|
@@ -114,6 +114,7 @@ export function searchCommandManifest(manifest, query, requestedLimit = DEFAULT_
|
|
|
114
114
|
const matches = tokens.length === 0
|
|
115
115
|
? []
|
|
116
116
|
: manifest.commands
|
|
117
|
+
.filter((command) => !command.hidden)
|
|
117
118
|
.map((command) => ({ command, score: scoreCommand(command, tokens, route) }))
|
|
118
119
|
.filter((entry) => entry.score > 0)
|
|
119
120
|
.sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name));
|
|
@@ -144,6 +145,7 @@ export function getCommandManifestEntry(manifest, exactName) {
|
|
|
144
145
|
export function suggestCommandNames(manifest, query, limit = 3) {
|
|
145
146
|
const tokens = tokenizeCommandQuery(query);
|
|
146
147
|
return manifest.commands
|
|
148
|
+
.filter((command) => !command.hidden)
|
|
147
149
|
.map((command) => ({ command, score: scoreCommand(command, tokens, null) }))
|
|
148
150
|
.filter((entry) => entry.score > 0)
|
|
149
151
|
.sort((a, b) => b.score - a.score || a.command.name.localeCompare(b.command.name))
|
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredential
|
|
|
20
20
|
import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
|
|
21
21
|
import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, listLocalMirrors, localPageSha256, markMirrorStale, mirrorExists, pageFilePath, planMirrorPush, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
|
|
22
22
|
import { waitForCliRun } from "./run-wait.js";
|
|
23
|
-
import { assertModeFlagsExclusive, parseJsonObject, readJsonObjectOption, readPositiveInt, readRecordString, resolveLiveDryRunMode, } from "./cli-values.js";
|
|
23
|
+
import { assertModeFlagsExclusive, parseKeyValuePairs, parseJsonObject, readJsonObjectOption, readPositiveInt, readRecordString, resolveLiveDryRunMode, } from "./cli-values.js";
|
|
24
24
|
import { formatAiPromptPreviewNotice } from "./column-run-notices.js";
|
|
25
25
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
26
26
|
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
@@ -452,6 +452,20 @@ function writeDryRunNotice(data) {
|
|
|
452
452
|
if (typeof block.next_step === "string")
|
|
453
453
|
process.stderr.write(`${block.next_step}\n`);
|
|
454
454
|
}
|
|
455
|
+
// An `oxy_live_` key can only ever answer for the one workspace it is bound to,
|
|
456
|
+
// so `orgs list` returns a single row and `orgs use` refuses — both truthfully,
|
|
457
|
+
// and both indistinguishable from "this account has one workspace" if you only
|
|
458
|
+
// read stdout. The server says which credential answered and whether it can
|
|
459
|
+
// switch; mirror that on stderr, the same split writeDryRunNotice uses, so the
|
|
460
|
+
// machine-read envelope stays clean. This is the line that would have stopped a
|
|
461
|
+
// user from minting a 21st key that also could not switch.
|
|
462
|
+
function writeCredentialNotice(credential) {
|
|
463
|
+
if (!credential || credential.switchable !== false)
|
|
464
|
+
return;
|
|
465
|
+
if (typeof credential.message === "string" && credential.message.trim()) {
|
|
466
|
+
process.stderr.write(`${credential.message}\n`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
455
469
|
// A sender-profile write that could not durably host the photo still succeeds —
|
|
456
470
|
// the profile is saved, just with a picture no inbox vendor will ever be able to
|
|
457
471
|
// fetch. That is the one failure mode nobody notices until the mailboxes ship
|
|
@@ -2494,6 +2508,7 @@ export function createProgram() {
|
|
|
2494
2508
|
.option("--token <token>", "CLI API token created in the Oxygen dashboard.")
|
|
2495
2509
|
.option("--api-url <url>", "Oxygen API URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
|
|
2496
2510
|
.option("--profile <name>", "Store credentials under a named CLI profile and make it active.")
|
|
2511
|
+
.option("--org <organization>", "Workspace to land in after logging in (id or slug). Needs a user key, which works across every workspace you belong to; a workspace-bound key can only land in its own.")
|
|
2497
2512
|
.option("--force", "Repoint an existing profile at a different API host. Refused without this flag.")
|
|
2498
2513
|
.option("--no-browser", "Skip the browser handoff and paste the token manually.")
|
|
2499
2514
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -2638,7 +2653,7 @@ export function createProgram() {
|
|
|
2638
2653
|
.action(async (options) => {
|
|
2639
2654
|
const outputOptions = { ...options, json: options.json || Boolean(activationCommand.opts().json) };
|
|
2640
2655
|
await handleAsyncAction("activation state", outputOptions, () => requestOxygen("/api/cli/activation/state"));
|
|
2641
|
-
}));
|
|
2656
|
+
}), { hidden: true });
|
|
2642
2657
|
const commandsCommand = program
|
|
2643
2658
|
.command("commands")
|
|
2644
2659
|
.description("Discover CLI commands. With no subcommand, print the backwards-compatible full manifest.")
|
|
@@ -2760,7 +2775,7 @@ export function createProgram() {
|
|
|
2760
2775
|
.description("List organizations available to the current CLI identity.")
|
|
2761
2776
|
.option("--json", "Print a JSON envelope.")
|
|
2762
2777
|
.action(async (options) => {
|
|
2763
|
-
await
|
|
2778
|
+
await handleOrgsListAction(options);
|
|
2764
2779
|
}))
|
|
2765
2780
|
.addCommand(new Command("use")
|
|
2766
2781
|
.description("Select the active organization for this CLI profile.")
|
|
@@ -7093,6 +7108,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7093
7108
|
.addCommand(new Command("add")
|
|
7094
7109
|
.description("Add a nullable column to a workspace table. Writes the definition only — this never runs the column and never spends credits; use `columns run` for that, with --dry-run first to see the cost.")
|
|
7095
7110
|
.argument("<table>", "Table id or slug.")
|
|
7111
|
+
.option("--preset <preset>", "Add a pre-built enrichment bundle instead of one column: `person_enrich` (one LinkedIn profile lookup, then headline, bio, location and followers for free) or `company_enrich` (domain, LinkedIn page, headcount, industry and full profile, cascading across providers until one answers). Oxygen finds the identity column itself — override with --input. Creating the columns is free; run them afterwards, --dry-run first.")
|
|
7112
|
+
.option("--input <slot=column...>", "Bind a preset input to an exact column, e.g. --input url=linkedin_url, or --input company_name=account --input domain=website. Repeatable. Only needed when the automatic match is wrong or missing.", collectRepeatable, [])
|
|
7096
7113
|
.option("--label <label>", "Display label for the new column. Required unless --prompt-key supplies a default title.")
|
|
7097
7114
|
.option("--key <key>", "Optional stable column key. Defaults to a normalized label.")
|
|
7098
7115
|
.option("--data-type <type>", "Column data type: text, numeric, boolean, jsonb, or timestamptz.")
|
|
@@ -7128,6 +7145,23 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7128
7145
|
// skipcq: JS-R1005 — intentional per-option branching to assemble the columns-add request body
|
|
7129
7146
|
.action(async (table, options) => {
|
|
7130
7147
|
await handleAsyncAction("columns add", options, async () => {
|
|
7148
|
+
// A preset names its own columns, so --label does not apply to it.
|
|
7149
|
+
const preset = readOption(options.preset);
|
|
7150
|
+
if (preset) {
|
|
7151
|
+
if (options.label || options.promptKey || options.definitionJson) {
|
|
7152
|
+
throw new OxygenError("invalid_request", "--preset builds its own column set. Drop --label/--prompt-key/--definition-json, or add the column by hand without --preset.", { exitCode: 1 });
|
|
7153
|
+
}
|
|
7154
|
+
const presetBody = { table, preset };
|
|
7155
|
+
const inputs = parseKeyValuePairs(options.input ?? []);
|
|
7156
|
+
if (Object.keys(inputs).length > 0)
|
|
7157
|
+
presetBody.inputs = inputs;
|
|
7158
|
+
if (options.key)
|
|
7159
|
+
presetBody.payload_column = options.key;
|
|
7160
|
+
return requestOxygen("/api/cli/tables/columns", {
|
|
7161
|
+
method: "POST",
|
|
7162
|
+
body: presetBody,
|
|
7163
|
+
});
|
|
7164
|
+
}
|
|
7131
7165
|
if (!options.promptKey && !options.label) {
|
|
7132
7166
|
throw new OxygenError("invalid_request", "--label is required.", { exitCode: 1 });
|
|
7133
7167
|
}
|
|
@@ -12619,6 +12653,53 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
12619
12653
|
const suffix = params.toString();
|
|
12620
12654
|
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/enrollments${suffix ? `?${suffix}` : ""}`);
|
|
12621
12655
|
});
|
|
12656
|
+
}))
|
|
12657
|
+
.addCommand(new Command("contacts")
|
|
12658
|
+
.description("List a launched campaign's table-backed contacts with full source variables and durable contacted/not-contacted status. Contacts removed from this campaign are hidden; source rows remain intact.")
|
|
12659
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
12660
|
+
.option("--contact-state <state>", "Filter: all, contacted, or not_contacted.", "all")
|
|
12661
|
+
.option("--limit <n>", "Maximum contacts to return (1-500).", "100")
|
|
12662
|
+
.option("--json", "Print a JSON envelope.")
|
|
12663
|
+
.action(async (sequence, options) => {
|
|
12664
|
+
await handleAsyncAction("sequences contacts", options, () => {
|
|
12665
|
+
const contactState = readOption(options.contactState) ?? "all";
|
|
12666
|
+
if (!["all", "contacted", "not_contacted"].includes(contactState)) {
|
|
12667
|
+
throw new Error("--contact-state must be all, contacted, or not_contacted.");
|
|
12668
|
+
}
|
|
12669
|
+
const limit = readPositiveInt(options.limit);
|
|
12670
|
+
if (limit === undefined || limit > 500) {
|
|
12671
|
+
throw new Error("--limit must be between 1 and 500.");
|
|
12672
|
+
}
|
|
12673
|
+
const params = new URLSearchParams({
|
|
12674
|
+
contact_state: contactState,
|
|
12675
|
+
limit: String(limit),
|
|
12676
|
+
});
|
|
12677
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/contacts?${params.toString()}`);
|
|
12678
|
+
});
|
|
12679
|
+
}))
|
|
12680
|
+
.addCommand(new Command("contacts-remove")
|
|
12681
|
+
.description("Remove selected source-table rows from this campaign only. Preview by default; --approved stops pending enrollment work and queues delegated-provider cleanup while preserving source rows and historical analytics.")
|
|
12682
|
+
.argument("<sequence>", "Sequence id or slug.")
|
|
12683
|
+
.requiredOption("--table-row-ids <ids>", "Comma-separated source table row UUIDs (maximum 500).")
|
|
12684
|
+
.option("--approved", "Apply the previewed campaign-only removal.")
|
|
12685
|
+
.option("--json", "Print a JSON envelope.")
|
|
12686
|
+
.action(async (sequence, options) => {
|
|
12687
|
+
await handleAsyncAction("sequences contacts-remove", options, () => {
|
|
12688
|
+
const tableRowIds = options.tableRowIds
|
|
12689
|
+
.split(",")
|
|
12690
|
+
.map((value) => value.trim())
|
|
12691
|
+
.filter(Boolean);
|
|
12692
|
+
if (tableRowIds.length === 0 || tableRowIds.length > 500) {
|
|
12693
|
+
throw new Error("--table-row-ids must contain between 1 and 500 row UUIDs.");
|
|
12694
|
+
}
|
|
12695
|
+
return requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/contacts`, {
|
|
12696
|
+
method: "POST",
|
|
12697
|
+
body: {
|
|
12698
|
+
table_row_ids: tableRowIds,
|
|
12699
|
+
approved: options.approved === true,
|
|
12700
|
+
},
|
|
12701
|
+
});
|
|
12702
|
+
});
|
|
12622
12703
|
}))
|
|
12623
12704
|
.addCommand(new Command("reconcile")
|
|
12624
12705
|
.description("Safely reconcile one ambiguous LinkedIn invite/message from provider reads. Preview-only unless --approved; never resends.")
|
|
@@ -13944,6 +14025,50 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
13944
14025
|
},
|
|
13945
14026
|
});
|
|
13946
14027
|
});
|
|
14028
|
+
}))
|
|
14029
|
+
.addCommand(new Command("auto-enroll")
|
|
14030
|
+
.description("Standing permission to connect NEW eligible mailboxes to EmailGuard automatically, so monitoring keeps up with the fleet instead of needing a manual connect per inbox. Enrolment is a RECURRING monthly charge per inbox, so arming is approval-gated and always bounded by a per-cycle credit ceiling. Without --arm/--disarm/--run this reports the current state (0 credits).")
|
|
14031
|
+
.option("--arm", "Arm the standing permission. Requires --max-credits; previews unless --approved.")
|
|
14032
|
+
.option("--disarm", "Turn it off. Existing subscriptions are untouched. 0 credits, no approval needed.")
|
|
14033
|
+
.option("--run", "Enrol one bounded batch now.")
|
|
14034
|
+
.option("--dry-run", "With --run: report what would be enrolled without connecting or charging.")
|
|
14035
|
+
.option("--max-credits <n>", "Per-billing-cycle credit ceiling this permission may spend (required with --arm).")
|
|
14036
|
+
.option("--approved", "Actually arm (otherwise --arm returns a preview).")
|
|
14037
|
+
.option("--json", "Print a JSON envelope.")
|
|
14038
|
+
.action(async (options) => {
|
|
14039
|
+
await handleAsyncAction("mailboxes emailguard auto-enroll", options, () => {
|
|
14040
|
+
const path = "/api/cli/mailboxes/emailguard/auto-enroll";
|
|
14041
|
+
if (options.disarm) {
|
|
14042
|
+
return requestOxygen(path, {
|
|
14043
|
+
method: "POST",
|
|
14044
|
+
body: { enabled: false },
|
|
14045
|
+
});
|
|
14046
|
+
}
|
|
14047
|
+
if (options.arm) {
|
|
14048
|
+
const ceiling = readOption(options.maxCredits);
|
|
14049
|
+
if (!ceiling) {
|
|
14050
|
+
throw new Error("--max-credits <n> is required with --arm: a standing permission without a ceiling would be unlimited recurring spend authority.");
|
|
14051
|
+
}
|
|
14052
|
+
return requestOxygen(path, {
|
|
14053
|
+
method: "POST",
|
|
14054
|
+
body: {
|
|
14055
|
+
enabled: true,
|
|
14056
|
+
max_credits_per_cycle: Number(ceiling),
|
|
14057
|
+
...(options.approved ? { approved: true } : {}),
|
|
14058
|
+
},
|
|
14059
|
+
});
|
|
14060
|
+
}
|
|
14061
|
+
if (options.run) {
|
|
14062
|
+
return requestOxygen(path, {
|
|
14063
|
+
method: "POST",
|
|
14064
|
+
body: {
|
|
14065
|
+
run: true,
|
|
14066
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
14067
|
+
},
|
|
14068
|
+
});
|
|
14069
|
+
}
|
|
14070
|
+
return requestOxygen(path);
|
|
14071
|
+
});
|
|
13947
14072
|
})))
|
|
13948
14073
|
.addCommand(new Command("delegation")
|
|
13949
14074
|
.description("Google sending-domain delegation only — not Microsoft OAuth and not a credential-file handoff to OXYGEN Warm-up or EmailGuard. Reports covered domains plus the exact client id/scopes; --probe mints a real token per mailbox. Read-only, sends no mail, 0 Oxygen credits.")
|
|
@@ -19576,6 +19701,12 @@ async function runAuthDoctor() {
|
|
|
19576
19701
|
}))
|
|
19577
19702
|
: { ok: false, skipped: true, reason: "no_credentials" };
|
|
19578
19703
|
const cachedIdentity = credentials?.identity ?? null;
|
|
19704
|
+
const cachedOrganization = credentials?.activeOrganization ?? cachedIdentity?.organization ?? null;
|
|
19705
|
+
// The live read wins over the stored `authKind`: a profile written by an older
|
|
19706
|
+
// CLI can carry no kind at all, and the server always knows.
|
|
19707
|
+
const liveAuthType = "ok" in identity && identity.ok && typeof identity.data.authType === "string"
|
|
19708
|
+
? identity.data.authType
|
|
19709
|
+
: null;
|
|
19579
19710
|
return {
|
|
19580
19711
|
profile: context?.resolution.exists ? context.resolution.name : null,
|
|
19581
19712
|
profile_source: context?.source ?? null,
|
|
@@ -19583,13 +19714,24 @@ async function runAuthDoctor() {
|
|
|
19583
19714
|
api_url: apiUrl,
|
|
19584
19715
|
auth_kind: credentials?.authKind ?? null,
|
|
19585
19716
|
token_fingerprint: credentials ? formatFingerprint(createCredentialFingerprint(credentials.token)) : null,
|
|
19586
|
-
cached_organization:
|
|
19717
|
+
cached_organization: cachedOrganization,
|
|
19587
19718
|
cached_user: cachedIdentity?.user ?? null,
|
|
19588
19719
|
profile_error: profileError,
|
|
19720
|
+
org_switching: {
|
|
19721
|
+
switchable: resolveOrgSwitchability(liveAuthType ?? credentials?.authKind ?? null),
|
|
19722
|
+
current_organization: cachedOrganization,
|
|
19723
|
+
},
|
|
19589
19724
|
health,
|
|
19590
19725
|
identity,
|
|
19591
19726
|
};
|
|
19592
19727
|
}
|
|
19728
|
+
function resolveOrgSwitchability(authKind) {
|
|
19729
|
+
if (authKind === "user_session" || authKind === "oauth")
|
|
19730
|
+
return true;
|
|
19731
|
+
if (authKind === "org_api_key")
|
|
19732
|
+
return false;
|
|
19733
|
+
return null;
|
|
19734
|
+
}
|
|
19593
19735
|
async function runAuthDoctorCheck(fn) {
|
|
19594
19736
|
try {
|
|
19595
19737
|
return { ok: true, data: await fn() };
|
|
@@ -19606,31 +19748,79 @@ async function runAuthDoctorCheck(fn) {
|
|
|
19606
19748
|
};
|
|
19607
19749
|
}
|
|
19608
19750
|
}
|
|
19751
|
+
async function handleOrgsListAction(options) {
|
|
19752
|
+
try {
|
|
19753
|
+
const data = await requestOxygen("/api/cli/orgs");
|
|
19754
|
+
emitSuccess("orgs list", data, options);
|
|
19755
|
+
writeCredentialNotice(data.credential);
|
|
19756
|
+
}
|
|
19757
|
+
catch (error) {
|
|
19758
|
+
emitCliFailure("orgs list", error);
|
|
19759
|
+
}
|
|
19760
|
+
}
|
|
19761
|
+
/**
|
|
19762
|
+
* Write a completed org selection back onto the stored profile.
|
|
19763
|
+
*
|
|
19764
|
+
* Shared by `orgs use` and `oxygen login --org` so the two-step (log in, then
|
|
19765
|
+
* switch) and the one-step collapse cannot drift apart.
|
|
19766
|
+
*
|
|
19767
|
+
* Gated on the credential being one that can actually carry a selection: an
|
|
19768
|
+
* org-bound key's active workspace is a property of the key, not of the profile,
|
|
19769
|
+
* so persisting a different one would only produce a profile that lies about
|
|
19770
|
+
* where its commands land.
|
|
19771
|
+
*/
|
|
19772
|
+
async function persistOrganizationSelection(input) {
|
|
19773
|
+
const { profile, credentials, organization } = input;
|
|
19774
|
+
if (!profile || !credentials || !organization)
|
|
19775
|
+
return false;
|
|
19776
|
+
if (credentials.authKind !== "user_session" && !credentials.token.startsWith("oxy_user_")) {
|
|
19777
|
+
return false;
|
|
19778
|
+
}
|
|
19779
|
+
await updateActiveOrganizationForProfile(profile, storedOrganizationFromOption(organization));
|
|
19780
|
+
return true;
|
|
19781
|
+
}
|
|
19782
|
+
/** Select an organization server-side, then persist it. Used by `login --org`. */
|
|
19783
|
+
async function selectOrganizationForProfile(input) {
|
|
19784
|
+
const data = await requestOxygen("/api/cli/orgs/select", {
|
|
19785
|
+
method: "POST",
|
|
19786
|
+
body: { organization: input.organization },
|
|
19787
|
+
credentials: input.credentials,
|
|
19788
|
+
});
|
|
19789
|
+
const selectionPersisted = await persistOrganizationSelection({
|
|
19790
|
+
profile: input.profile,
|
|
19791
|
+
credentials: input.credentials,
|
|
19792
|
+
organization: data.currentOrganization,
|
|
19793
|
+
});
|
|
19794
|
+
return { data, selectionPersisted };
|
|
19795
|
+
}
|
|
19609
19796
|
async function handleOrgUseAction(organization, options, command) {
|
|
19610
19797
|
try {
|
|
19798
|
+
// Credentials stay ambient here: an OXYGEN_API_KEY in the environment
|
|
19799
|
+
// outranks the stored profile inside requestOxygen, and passing the profile
|
|
19800
|
+
// explicitly would silently select with the wrong one.
|
|
19611
19801
|
const data = await requestOxygen("/api/cli/orgs/select", {
|
|
19612
19802
|
method: "POST",
|
|
19613
19803
|
body: { organization },
|
|
19614
19804
|
});
|
|
19615
19805
|
const context = await resolveActiveProfileWithSource();
|
|
19616
|
-
const
|
|
19617
|
-
const
|
|
19618
|
-
|
|
19619
|
-
|
|
19620
|
-
data.currentOrganization
|
|
19621
|
-
|
|
19622
|
-
await updateActiveOrganizationForProfile(context.resolution.name, storedOrganizationFromOption(data.currentOrganization));
|
|
19623
|
-
}
|
|
19806
|
+
const profile = context.resolution.exists ? context.resolution.name : null;
|
|
19807
|
+
const selectionPersisted = await persistOrganizationSelection({
|
|
19808
|
+
profile,
|
|
19809
|
+
credentials: context.resolution.credentials,
|
|
19810
|
+
organization: data.currentOrganization,
|
|
19811
|
+
});
|
|
19624
19812
|
const result = {
|
|
19625
19813
|
...data,
|
|
19626
|
-
profile
|
|
19627
|
-
selection_persisted:
|
|
19814
|
+
profile,
|
|
19815
|
+
selection_persisted: selectionPersisted,
|
|
19628
19816
|
};
|
|
19629
19817
|
if (options.json) {
|
|
19630
19818
|
writeJson(success(command, result));
|
|
19631
|
-
return;
|
|
19632
19819
|
}
|
|
19633
|
-
|
|
19820
|
+
else {
|
|
19821
|
+
writeJson(result);
|
|
19822
|
+
}
|
|
19823
|
+
writeCredentialNotice(data.credential);
|
|
19634
19824
|
}
|
|
19635
19825
|
catch (error) {
|
|
19636
19826
|
emitCliFailure(command, error);
|
|
@@ -19906,14 +20096,39 @@ async function login(options) {
|
|
|
19906
20096
|
profile: chosenProfile,
|
|
19907
20097
|
allowHostChange: options.force === true,
|
|
19908
20098
|
});
|
|
20099
|
+
// `--org` collapses the two-step every agency user was doing by hand (`login`,
|
|
20100
|
+
// then `orgs use`). It is skipped when the credential already landed in the
|
|
20101
|
+
// requested workspace, so a workspace-bound key naming its own workspace
|
|
20102
|
+
// succeeds instead of being refused for asking to stay where it is.
|
|
20103
|
+
const requestedOrg = readOption(options.org) ?? process.env.OXYGEN_ORG?.trim() ?? null;
|
|
20104
|
+
let activeOrganization = loginIdentity.activeOrganization;
|
|
20105
|
+
let organizations = loginIdentity.organizations;
|
|
20106
|
+
if (requestedOrg && !matchesStoredOrganization(activeOrganization, requestedOrg)) {
|
|
20107
|
+
const selected = await selectOrganizationForProfile({
|
|
20108
|
+
organization: requestedOrg,
|
|
20109
|
+
profile,
|
|
20110
|
+
credentials,
|
|
20111
|
+
});
|
|
20112
|
+
activeOrganization = storedOrganizationFromOption(selected.data.currentOrganization);
|
|
20113
|
+
organizations = organizations.map((organization) => ({
|
|
20114
|
+
...organization,
|
|
20115
|
+
selected: organization.id === selected.data.currentOrganization.id,
|
|
20116
|
+
}));
|
|
20117
|
+
}
|
|
19909
20118
|
const skillsInstall = await runAutomaticSkillsInstall({ apiUrl: credentials.apiUrl, credentials });
|
|
19910
20119
|
// Opt-in eager mirror of every reachable workspace, off by default so login
|
|
19911
20120
|
// stays fast and non-surprising. Best-effort: a sync failure never fails login.
|
|
19912
20121
|
if (process.env.OXYGEN_KNOWLEDGE_AUTOSYNC === "1") {
|
|
19913
20122
|
await runKnowledgeMirrorSyncAll({ ifStale: true }).catch(() => undefined);
|
|
19914
20123
|
}
|
|
20124
|
+
const resolved = {
|
|
20125
|
+
...loginIdentity,
|
|
20126
|
+
activeOrganization,
|
|
20127
|
+
organizations,
|
|
20128
|
+
...(requestedOrg ? { selectionRequired: false } : {}),
|
|
20129
|
+
};
|
|
19915
20130
|
if (!options.json) {
|
|
19916
|
-
process.stdout.write(formatLoginSuccessForResolved(
|
|
20131
|
+
process.stdout.write(formatLoginSuccessForResolved(resolved, profile, { renamed, skillsInstall }));
|
|
19917
20132
|
const hint = await buildPostLoginHint(profile);
|
|
19918
20133
|
if (hint)
|
|
19919
20134
|
process.stdout.write(hint);
|
|
@@ -19922,13 +20137,24 @@ async function login(options) {
|
|
|
19922
20137
|
logged_in: true,
|
|
19923
20138
|
profile,
|
|
19924
20139
|
api_url: credentials.apiUrl,
|
|
19925
|
-
user:
|
|
19926
|
-
organization:
|
|
19927
|
-
organizations:
|
|
19928
|
-
selection_required:
|
|
20140
|
+
user: resolved.user,
|
|
20141
|
+
organization: resolved.activeOrganization,
|
|
20142
|
+
organizations: resolved.organizations,
|
|
20143
|
+
selection_required: resolved.selectionRequired,
|
|
19929
20144
|
skills_install: skillsInstall,
|
|
19930
20145
|
};
|
|
19931
20146
|
}
|
|
20147
|
+
// `--org` accepts whatever `orgs list` prints, so the match has to accept the
|
|
20148
|
+
// same handles the server does — minus clerkOrgId, which stored profiles do not
|
|
20149
|
+
// carry.
|
|
20150
|
+
function matchesStoredOrganization(organization, ref) {
|
|
20151
|
+
if (!organization)
|
|
20152
|
+
return false;
|
|
20153
|
+
const normalized = ref.trim();
|
|
20154
|
+
if (!normalized)
|
|
20155
|
+
return false;
|
|
20156
|
+
return organization.id === normalized || organization.slug === normalized;
|
|
20157
|
+
}
|
|
19932
20158
|
async function applyAuthToken(options) {
|
|
19933
20159
|
const token = readOption(options.token);
|
|
19934
20160
|
if (!token) {
|
|
@@ -20416,6 +20642,7 @@ function formatAuthDoctor(data) {
|
|
|
20416
20642
|
const org = data.cached_organization.slug ?? data.cached_organization.id;
|
|
20417
20643
|
lines.push(` ${styles.dim("Cached org")} ${data.cached_organization.name} (${org})`);
|
|
20418
20644
|
}
|
|
20645
|
+
lines.push(` ${styles.dim("Org switching")} ${formatOrgSwitching(data.org_switching)}`);
|
|
20419
20646
|
if (data.profile_error) {
|
|
20420
20647
|
lines.push(` ${styles.dim("Profile error")} ${data.profile_error.code}: ${data.profile_error.message}`);
|
|
20421
20648
|
}
|
|
@@ -20432,6 +20659,15 @@ function formatAuthDoctor(data) {
|
|
|
20432
20659
|
lines.push("");
|
|
20433
20660
|
return lines.join("\n");
|
|
20434
20661
|
}
|
|
20662
|
+
function formatOrgSwitching(orgSwitching) {
|
|
20663
|
+
if (orgSwitching.switchable === true)
|
|
20664
|
+
return "available — `oxygen orgs use <organization>`";
|
|
20665
|
+
if (orgSwitching.switchable === false) {
|
|
20666
|
+
const name = orgSwitching.current_organization?.name;
|
|
20667
|
+
return `locked to ${name ?? "this workspace"} — \`oxygen login\` for a switchable user key`;
|
|
20668
|
+
}
|
|
20669
|
+
return "unknown";
|
|
20670
|
+
}
|
|
20435
20671
|
function formatProfileOrgCell(profile) {
|
|
20436
20672
|
if (!profile.organization)
|
|
20437
20673
|
return "(unknown org — run `oxygen whoami` to refresh)";
|
|
@@ -37,21 +37,19 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
37
37
|
primitive: null,
|
|
38
38
|
owns: "First-run setup and interactive Workspace Copilot sessions over the shared primitive contracts.",
|
|
39
39
|
notFor: "Recurring deterministic automation or standing adaptive jobs; use Workflows or Agents.",
|
|
40
|
-
execution: "
|
|
40
|
+
execution: "Resolve factual onboarding state, then let the user choose a Recipe, Blueprint, or their own outcome before starting a hosted Copilot session.",
|
|
41
41
|
posture: "mixed",
|
|
42
|
-
gatewayTools: ["oxygen_whoami", "
|
|
43
|
-
gatewayCommands: ["whoami", "
|
|
42
|
+
gatewayTools: ["oxygen_whoami", "oxygen_recipes_list", "oxygen_blueprints_list", "oxygen_context_resolve", "oxygen_copilot_start"],
|
|
43
|
+
gatewayCommands: ["whoami", "recipes list", "blueprints list", "context resolve", "copilot start"],
|
|
44
44
|
skills: ["oxygen-quickstart", "oxygen-gtm"],
|
|
45
|
-
// `activation`
|
|
46
|
-
//
|
|
47
|
-
// Control question — and without the section on this card, the Copilot's very
|
|
48
|
-
// first tool call routes past the only read that answers it.
|
|
45
|
+
// `activation` remains an exact legacy API family for saved clients, but it
|
|
46
|
+
// has no recommended gateway and is not an intent term or first-touch path.
|
|
49
47
|
endpointSections: ["activation", "copilot", "home"],
|
|
50
48
|
// "next step" alone is deliberately absent: it is a phrase every primitive's
|
|
51
49
|
// users say ("next step of this workflow run"), and a 6-point multi-word hit
|
|
52
50
|
// would drag those queries here. The terms kept are the ones only a founder
|
|
53
51
|
// asking where they are in setup actually types.
|
|
54
|
-
intentTerms: ["onboard", "onboarding", "first run", "first day", "setup oxygen", "copilot", "assistant", "guided", "
|
|
52
|
+
intentTerms: ["onboard", "onboarding", "first run", "first day", "setup oxygen", "copilot", "assistant", "guided", "where am i", "next step in setup", "what do i do next"],
|
|
55
53
|
},
|
|
56
54
|
{
|
|
57
55
|
id: "billing-and-budgets",
|
|
@@ -171,7 +169,7 @@ export const OXYGEN_CAPABILITY_ROUTES = [
|
|
|
171
169
|
gatewayCommands: ["recipes list", "recipes show", "recipes install"],
|
|
172
170
|
skills: ["oxygen-recipes"],
|
|
173
171
|
endpointSections: ["blueprints", "recipes", "templates"],
|
|
174
|
-
intentTerms: ["recipe", "playbook", "proven play", "journey", "day 1", "day 7", "day 30", "blueprint", "template", "what should i do"],
|
|
172
|
+
intentTerms: ["recipe", "playbook", "proven play", "activation play", "setup play", "journey", "day 1", "day 7", "day 30", "blueprint", "template", "what should i do"],
|
|
175
173
|
},
|
|
176
174
|
{
|
|
177
175
|
id: "records",
|
|
@@ -7,14 +7,7 @@ export type CopilotJourney = {
|
|
|
7
7
|
/** The first user message a journey card seeds into a new session. */
|
|
8
8
|
seedMessage: string;
|
|
9
9
|
};
|
|
10
|
-
|
|
11
|
-
* The one journey a workspace with no play yet is pointed at. Exported as a named
|
|
12
|
-
* constant because every consumer that needs "the prescribed play" must key on the
|
|
13
|
-
* slug: the array is display-ordered, and `COPILOT_JOURNEYS[0]` would silently
|
|
14
|
-
* re-designate a different journey the first time someone reorders the cards.
|
|
15
|
-
* `home/suggestions.tsx` keys its glyphs the same way, for the same reason.
|
|
16
|
-
*/
|
|
17
|
-
export declare const PRESCRIBED_COPILOT_JOURNEY_SLUG = "inbound-led-outbound";
|
|
10
|
+
export declare const LINKEDIN_NETWORK_COPILOT_JOURNEY_SLUG = "inbound-led-outbound";
|
|
18
11
|
export declare const COPILOT_JOURNEYS: readonly CopilotJourney[];
|
|
19
12
|
export type CopilotJourneySlug = (typeof COPILOT_JOURNEYS)[number]["slug"];
|
|
20
13
|
export declare function getCopilotJourney(slug: string): CopilotJourney | null;
|
|
@@ -4,17 +4,12 @@
|
|
|
4
4
|
// Each journey is a thin descriptor over EXISTING advisory catalog Recipes:
|
|
5
5
|
// journeys add no runtime behavior, and every guardrail (never enroll/send/buy/
|
|
6
6
|
// change DNS without approval) rides those recipes' own approval gates.
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
* slug: the array is display-ordered, and `COPILOT_JOURNEYS[0]` would silently
|
|
11
|
-
* re-designate a different journey the first time someone reorders the cards.
|
|
12
|
-
* `home/suggestions.tsx` keys its glyphs the same way, for the same reason.
|
|
13
|
-
*/
|
|
14
|
-
export const PRESCRIBED_COPILOT_JOURNEY_SLUG = "inbound-led-outbound";
|
|
7
|
+
// Stable slug for sessions deliberately launched from the matching Recipe.
|
|
8
|
+
// It is never a default: first-touch surfaces stay outcome-neutral.
|
|
9
|
+
export const LINKEDIN_NETWORK_COPILOT_JOURNEY_SLUG = "inbound-led-outbound";
|
|
15
10
|
export const COPILOT_JOURNEYS = [
|
|
16
11
|
{
|
|
17
|
-
slug:
|
|
12
|
+
slug: LINKEDIN_NETWORK_COPILOT_JOURNEY_SLUG,
|
|
18
13
|
title: "Inbound-led outbound",
|
|
19
14
|
description: "Work the LinkedIn network you already have — connections and post engagers — as a capped warm motion.",
|
|
20
15
|
// The network-first play leads: connections and post engagers are the two
|
|
@@ -29,10 +24,8 @@ export const COPILOT_JOURNEYS = [
|
|
|
29
24
|
// Deliberately free of steps, commands, table names, and caps: encoding
|
|
30
25
|
// mechanics here would fork a second, unversioned copy of the play that no
|
|
31
26
|
// catalog gate can lint (ADR 0013 §11 — a journey is a thin pointer at
|
|
32
|
-
// advisory recipes, never a hardcoded kit).
|
|
33
|
-
|
|
34
|
-
// it is about where THIS workspace already stands, which the catalog cannot know.
|
|
35
|
-
seedMessage: "I want to turn the LinkedIn network I already have — my connections and the people who engage with my posts — into a small, safe, approval-gated outreach motion. First read my workspace's current activation state and tell me where I already am, then resume from there instead of starting over. Follow the linkedin-network-first-motion recipe for the play itself. Do not connect an account, enroll anyone, send anything, or spend credits without asking me first.",
|
|
27
|
+
// advisory recipes, never a hardcoded kit).
|
|
28
|
+
seedMessage: "I chose the linkedin-network-first-motion recipe. Help me turn the LinkedIn network I already have — my connections and the people who engage with my posts — into a small, safe, approval-gated outreach motion. Inspect the relevant workspace primitives, follow the recipe, and resume from durable evidence instead of starting over. Do not connect an account, enroll anyone, send anything, or spend credits without asking me first.",
|
|
36
29
|
},
|
|
37
30
|
{
|
|
38
31
|
slug: "tam-sourcing",
|
|
@@ -61,3 +61,31 @@ export declare function sendingSeatMonthlyPriceCents(key: SendingSeatKey): numbe
|
|
|
61
61
|
export declare function sendingSeatSubtotalCents(key: SendingSeatKey, quantity: number): number;
|
|
62
62
|
/** "$30.00" — seat prices are whole cents, so this never rounds. */
|
|
63
63
|
export declare function formatSeatPrice(cents: number): string;
|
|
64
|
+
/**
|
|
65
|
+
* Whether the connect-time seat requirement is armed.
|
|
66
|
+
*
|
|
67
|
+
* FAIL-CLOSED TO OFF, deliberately, and shipped that way. Arming this refuses a
|
|
68
|
+
* sender connection that succeeds today, so it is a customer-visible enforcement
|
|
69
|
+
* change and belongs to a human decision, not to whenever the code happens to
|
|
70
|
+
* deploy. Same posture as the free-tier entitlement flag.
|
|
71
|
+
*
|
|
72
|
+
* It is also the switch that decides whether grandfathered organizations are
|
|
73
|
+
* capped at their cutover count or effectively unlimited: with the gate off,
|
|
74
|
+
* everyone connects freely; with it on, an org may add senders beyond its legacy
|
|
75
|
+
* entitlement only by buying seats. Do not arm it until that call is made and
|
|
76
|
+
* the legacy backfill has run for every channel being enforced.
|
|
77
|
+
*/
|
|
78
|
+
export declare function sendingSeatGateEnabled(env?: NodeJS.ProcessEnv): boolean;
|
|
79
|
+
export declare const SENDING_SEAT_GATE_ENABLED_ENV_VAR = "OXYGEN_SENDING_SEAT_GATE_ENABLED";
|
|
80
|
+
/**
|
|
81
|
+
* Which seat kind a connect request consumes, by CLI path prefix.
|
|
82
|
+
*
|
|
83
|
+
* MAILBOXES ARE DELIBERATELY ABSENT. Email mailboxes live in the TENANT
|
|
84
|
+
* database, so the control-plane grandfathering backfill cannot see them and did
|
|
85
|
+
* not cover them. Enforcing a seat on mailbox connect today would refuse
|
|
86
|
+
* customers who already have mailboxes attached — the precise failure this whole
|
|
87
|
+
* grandfathering design exists to prevent. Add mailboxes here only once a
|
|
88
|
+
* tenant-aware backfill has granted their legacy entitlements.
|
|
89
|
+
*/
|
|
90
|
+
export declare const SEAT_KIND_BY_CONNECT_PREFIX: ReadonlyArray<readonly [prefix: string, seatKey: SendingSeatKey]>;
|
|
91
|
+
export declare function seatKindForConnectPath(pathname: string): SendingSeatKey | null;
|
|
@@ -99,3 +99,46 @@ export function sendingSeatSubtotalCents(key, quantity) {
|
|
|
99
99
|
export function formatSeatPrice(cents) {
|
|
100
100
|
return `$${(cents / 100).toFixed(2)}`;
|
|
101
101
|
}
|
|
102
|
+
/**
|
|
103
|
+
* Whether the connect-time seat requirement is armed.
|
|
104
|
+
*
|
|
105
|
+
* FAIL-CLOSED TO OFF, deliberately, and shipped that way. Arming this refuses a
|
|
106
|
+
* sender connection that succeeds today, so it is a customer-visible enforcement
|
|
107
|
+
* change and belongs to a human decision, not to whenever the code happens to
|
|
108
|
+
* deploy. Same posture as the free-tier entitlement flag.
|
|
109
|
+
*
|
|
110
|
+
* It is also the switch that decides whether grandfathered organizations are
|
|
111
|
+
* capped at their cutover count or effectively unlimited: with the gate off,
|
|
112
|
+
* everyone connects freely; with it on, an org may add senders beyond its legacy
|
|
113
|
+
* entitlement only by buying seats. Do not arm it until that call is made and
|
|
114
|
+
* the legacy backfill has run for every channel being enforced.
|
|
115
|
+
*/
|
|
116
|
+
export function sendingSeatGateEnabled(env = process.env) {
|
|
117
|
+
const raw = env[SENDING_SEAT_GATE_ENABLED_ENV_VAR];
|
|
118
|
+
if (!raw)
|
|
119
|
+
return false;
|
|
120
|
+
const normalized = raw.trim().toLowerCase();
|
|
121
|
+
return normalized === "1" || normalized === "true" || normalized === "yes" || normalized === "on";
|
|
122
|
+
}
|
|
123
|
+
export const SENDING_SEAT_GATE_ENABLED_ENV_VAR = "OXYGEN_SENDING_SEAT_GATE_ENABLED";
|
|
124
|
+
/**
|
|
125
|
+
* Which seat kind a connect request consumes, by CLI path prefix.
|
|
126
|
+
*
|
|
127
|
+
* MAILBOXES ARE DELIBERATELY ABSENT. Email mailboxes live in the TENANT
|
|
128
|
+
* database, so the control-plane grandfathering backfill cannot see them and did
|
|
129
|
+
* not cover them. Enforcing a seat on mailbox connect today would refuse
|
|
130
|
+
* customers who already have mailboxes attached — the precise failure this whole
|
|
131
|
+
* grandfathering design exists to prevent. Add mailboxes here only once a
|
|
132
|
+
* tenant-aware backfill has granted their legacy entitlements.
|
|
133
|
+
*/
|
|
134
|
+
export const SEAT_KIND_BY_CONNECT_PREFIX = [
|
|
135
|
+
["/api/cli/senders/connect", "linkedin"],
|
|
136
|
+
["/api/cli/whatsapp/connect", "whatsapp"],
|
|
137
|
+
];
|
|
138
|
+
export function seatKindForConnectPath(pathname) {
|
|
139
|
+
for (const [prefix, seatKey] of SEAT_KIND_BY_CONNECT_PREFIX) {
|
|
140
|
+
if (pathname === prefix || pathname.startsWith(`${prefix}/`))
|
|
141
|
+
return seatKey;
|
|
142
|
+
}
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.861.0";
|
|
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.
|
|
1
|
+
export const OXYGEN_VERSION = "1.861.0";
|
|
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:
|