@oxygen-agent/cli 1.275.2 → 1.284.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.
- package/README.md +1 -1
- package/dist/command-manifest.d.ts +30 -0
- package/dist/command-manifest.js +70 -0
- package/dist/help.d.ts +2 -0
- package/dist/help.js +79 -0
- package/dist/http-client.d.ts +1 -0
- package/dist/http-client.js +13 -0
- package/dist/index.js +386 -24
- package/dist/knowledge-mirror.d.ts +22 -0
- package/dist/knowledge-mirror.js +56 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/billing.js +6 -0
- package/node_modules/@oxygen/shared/dist/cli-result.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/cli-result.js +65 -0
- package/node_modules/@oxygen/shared/dist/deprecation-registry.d.ts +12 -0
- package/node_modules/@oxygen/shared/dist/deprecation-registry.js +58 -0
- package/node_modules/@oxygen/shared/dist/knowledge-markdown.d.ts +25 -0
- package/node_modules/@oxygen/shared/dist/knowledge-markdown.js +74 -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
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
export type CommandManifestFlag = {
|
|
3
|
+
flags: string;
|
|
4
|
+
description: string;
|
|
5
|
+
required: boolean;
|
|
6
|
+
};
|
|
7
|
+
export type CommandManifestArgument = {
|
|
8
|
+
name: string;
|
|
9
|
+
required: boolean;
|
|
10
|
+
description: string;
|
|
11
|
+
};
|
|
12
|
+
export type CommandManifestEntry = {
|
|
13
|
+
name: string;
|
|
14
|
+
group: string;
|
|
15
|
+
description: string;
|
|
16
|
+
arguments: CommandManifestArgument[];
|
|
17
|
+
flags: CommandManifestFlag[];
|
|
18
|
+
spends_credits: boolean;
|
|
19
|
+
mutates: boolean;
|
|
20
|
+
json_supported: boolean;
|
|
21
|
+
hidden: boolean;
|
|
22
|
+
};
|
|
23
|
+
export type CommandManifest = {
|
|
24
|
+
binary: string;
|
|
25
|
+
version: string;
|
|
26
|
+
total: number;
|
|
27
|
+
exit_codes: Record<string, string>;
|
|
28
|
+
commands: CommandManifestEntry[];
|
|
29
|
+
};
|
|
30
|
+
export declare function buildCommandManifest(program: Command, binaryName: string): CommandManifest;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { CLI_EXIT_CODE_TABLE, OXYGEN_VERSION } from "@oxygen/shared";
|
|
2
|
+
const MUTATING_VERBS = new Set([
|
|
3
|
+
"add", "apply", "approve", "archive", "assign", "buy", "cancel", "clear",
|
|
4
|
+
"configure", "connect", "create", "delete", "disable", "dispatch", "draft",
|
|
5
|
+
"emit", "enable", "enroll", "grant", "import", "insert", "invite", "launch",
|
|
6
|
+
"login", "logout", "materialize", "merge", "migrate", "order", "pause",
|
|
7
|
+
"publish", "purchase", "push", "record", "register", "remove", "rename",
|
|
8
|
+
"reorder", "reply", "rerun", "reset", "resolve", "restore", "resume",
|
|
9
|
+
"retry", "retype", "revoke", "rotate", "run", "save", "schedule", "select",
|
|
10
|
+
"send", "set", "share", "start", "stop", "subscribe", "sync", "unpublish",
|
|
11
|
+
"unshare", "unsubscribe", "update", "upload", "upsert", "use", "withdraw",
|
|
12
|
+
"write",
|
|
13
|
+
]);
|
|
14
|
+
export function buildCommandManifest(program, binaryName) {
|
|
15
|
+
const commands = [];
|
|
16
|
+
for (const child of program.commands) {
|
|
17
|
+
collectCommand(child, [], commands);
|
|
18
|
+
}
|
|
19
|
+
commands.sort((a, b) => a.name.localeCompare(b.name));
|
|
20
|
+
return {
|
|
21
|
+
binary: binaryName,
|
|
22
|
+
version: OXYGEN_VERSION,
|
|
23
|
+
total: commands.length,
|
|
24
|
+
exit_codes: Object.fromEntries(Object.entries(CLI_EXIT_CODE_TABLE).map(([code, meaning]) => [code, meaning])),
|
|
25
|
+
commands,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function collectCommand(command, ancestors, out) {
|
|
29
|
+
const path = [...ancestors, command.name()];
|
|
30
|
+
const children = command.commands;
|
|
31
|
+
// Group commands (e.g. `oxygen tables`) exist to hold subcommands; only leaf
|
|
32
|
+
// commands are invocable work, so only leaves become manifest entries.
|
|
33
|
+
if (children.length === 0) {
|
|
34
|
+
out.push(toManifestEntry(command, path));
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
for (const child of children) {
|
|
38
|
+
collectCommand(child, path, out);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function toManifestEntry(command, path) {
|
|
42
|
+
const options = command.options.filter((option) => !option.hidden);
|
|
43
|
+
const flagStrings = options.map((option) => option.flags);
|
|
44
|
+
const leafVerb = path[path.length - 1] ?? "";
|
|
45
|
+
const verbPrefix = leafVerb.split("-")[0] ?? leafVerb;
|
|
46
|
+
return {
|
|
47
|
+
name: path.join(" "),
|
|
48
|
+
group: path[0] ?? "",
|
|
49
|
+
description: command.description(),
|
|
50
|
+
arguments: command.registeredArguments.map((argument) => ({
|
|
51
|
+
name: argument.name(),
|
|
52
|
+
required: argument.required,
|
|
53
|
+
description: argument.description,
|
|
54
|
+
})),
|
|
55
|
+
flags: options.map((option) => ({
|
|
56
|
+
flags: option.flags,
|
|
57
|
+
description: option.description,
|
|
58
|
+
required: option.mandatory,
|
|
59
|
+
})),
|
|
60
|
+
spends_credits: flagStrings.some((flags) => flags.includes("--approved") || flags.includes("--max-credits")),
|
|
61
|
+
mutates: MUTATING_VERBS.has(leafVerb) || MUTATING_VERBS.has(verbPrefix),
|
|
62
|
+
json_supported: flagStrings.some((flags) => flags.includes("--json")),
|
|
63
|
+
hidden: isHiddenCommand(command),
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function isHiddenCommand(command) {
|
|
67
|
+
// Commander keeps hidden state private; read it defensively so a library
|
|
68
|
+
// bump degrades to hidden:false instead of a crash.
|
|
69
|
+
return Boolean(command._hidden);
|
|
70
|
+
}
|
package/dist/help.d.ts
ADDED
package/dist/help.js
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { CLI_EXIT_CODE_TABLE } from "@oxygen/shared";
|
|
2
|
+
// Grouped `--help` for the ~70 top-level command groups: commander's default
|
|
3
|
+
// registration-order wall (login, auth, profiles, ... before any GTM noun) is
|
|
4
|
+
// the first thing every fresh agent reads, so order it by the OXYGEN OS layer
|
|
5
|
+
// a command belongs to and close with an agent quickstart + the reserved
|
|
6
|
+
// exit-code table. Commands missing from the map land in the last group so a
|
|
7
|
+
// new command never disappears from help.
|
|
8
|
+
const HELP_GROUPS = [
|
|
9
|
+
{
|
|
10
|
+
heading: "Get started & account:",
|
|
11
|
+
commands: [
|
|
12
|
+
"login", "auth", "profiles", "logout", "update", "api-keys", "whoami",
|
|
13
|
+
"onboarding", "status", "orgs", "commands", "skills", "session",
|
|
14
|
+
"support", "feedback",
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
heading: "Knowledge (context, wiki, recipes):",
|
|
19
|
+
commands: ["context", "knowledge", "prompts", "templates", "blueprints"],
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
heading: "Data (tables, CRM, dashboards):",
|
|
23
|
+
commands: [
|
|
24
|
+
"tables", "rows", "cells", "columns", "action-column", "enrich-column",
|
|
25
|
+
"enrichment", "table-runs", "table-ingestions", "runs", "crm",
|
|
26
|
+
"dashboards", "projects", "signup-leads",
|
|
27
|
+
],
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
heading: "Sourcing & search:",
|
|
31
|
+
commands: ["sourcing", "lead-sourcing", "search", "companies", "people", "find", "tools"],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
heading: "Outbound & channels:",
|
|
35
|
+
commands: [
|
|
36
|
+
"sequences", "senders", "inbox", "messages", "mailboxes",
|
|
37
|
+
"managed-inboxes", "deliverability", "domains", "email", "schedules",
|
|
38
|
+
"suppressions", "egress", "linkedin", "connections", "followers",
|
|
39
|
+
"viewers", "posts", "engagement", "whatsapp", "publishing", "notetaker",
|
|
40
|
+
"reviews",
|
|
41
|
+
],
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
heading: "Automation, billing & admin:",
|
|
45
|
+
commands: [
|
|
46
|
+
"workflows", "integrations", "custom-integrations", "billing", "budget",
|
|
47
|
+
"observability", "worker", "db", "admin", "directory",
|
|
48
|
+
],
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
const FALLBACK_HEADING = "Other commands:";
|
|
52
|
+
export function applyOxygenHelp(program, binaryName) {
|
|
53
|
+
const headingByCommand = new Map();
|
|
54
|
+
for (const group of HELP_GROUPS) {
|
|
55
|
+
for (const name of group.commands)
|
|
56
|
+
headingByCommand.set(name, group.heading);
|
|
57
|
+
}
|
|
58
|
+
for (const command of program.commands) {
|
|
59
|
+
command.helpGroup(headingByCommand.get(command.name()) ?? FALLBACK_HEADING);
|
|
60
|
+
}
|
|
61
|
+
const exitCodeLine = Object.entries(CLI_EXIT_CODE_TABLE)
|
|
62
|
+
.map(([code, meaning]) => `${code} ${meaning.split(" (")[0]}`)
|
|
63
|
+
.join(" · ");
|
|
64
|
+
program.addHelpText("afterAll", [
|
|
65
|
+
"",
|
|
66
|
+
"Agent quickstart:",
|
|
67
|
+
` 1. ${binaryName} login connect this terminal (or set OXYGEN_API_KEY)`,
|
|
68
|
+
` 2. ${binaryName} onboarding start --json load workspace context + the skill that teaches the GTM loops`,
|
|
69
|
+
` 3. ${binaryName} commands --json full machine-readable command grammar (this CLI's self-index)`,
|
|
70
|
+
"",
|
|
71
|
+
"Conventions:",
|
|
72
|
+
" --json prints a stable envelope {ok, data|error, meta}; errors always print it.",
|
|
73
|
+
" Commands that spend credits or write to external platforms are gated: live runs",
|
|
74
|
+
" require --approved --max-credits <n> and fail with a typed error until approved.",
|
|
75
|
+
" State-changing responses include a web_url deep-link to inspect the result.",
|
|
76
|
+
"",
|
|
77
|
+
`Exit codes: ${exitCodeLine}`,
|
|
78
|
+
].join("\n"));
|
|
79
|
+
}
|
package/dist/http-client.d.ts
CHANGED
package/dist/http-client.js
CHANGED
|
@@ -42,6 +42,17 @@ path, options = {}) {
|
|
|
42
42
|
if (options.body) {
|
|
43
43
|
headers["Content-Type"] = "application/json";
|
|
44
44
|
}
|
|
45
|
+
// Every mutating request carries an idempotency key. Routes that support
|
|
46
|
+
// replay dedupe on it; others ignore the header. On a timeout the key is
|
|
47
|
+
// echoed in error details so a re-run (OXYGEN_IDEMPOTENCY_KEY=<key>) can
|
|
48
|
+
// retry without duplicating the write once the route supports replay.
|
|
49
|
+
const method = options.method ?? "GET";
|
|
50
|
+
const idempotencyKey = method === "GET"
|
|
51
|
+
? undefined
|
|
52
|
+
: options.idempotencyKey ?? process.env.OXYGEN_IDEMPOTENCY_KEY ?? randomUUID();
|
|
53
|
+
if (idempotencyKey) {
|
|
54
|
+
headers["Idempotency-Key"] = idempotencyKey;
|
|
55
|
+
}
|
|
45
56
|
let response;
|
|
46
57
|
const timeoutMs = resolveRequestTimeoutMs(options.timeoutMs);
|
|
47
58
|
const timeout = createTimeoutSignal(timeoutMs);
|
|
@@ -64,6 +75,7 @@ path, options = {}) {
|
|
|
64
75
|
path,
|
|
65
76
|
timeout_ms: timeoutMs,
|
|
66
77
|
trace_id: traceId,
|
|
78
|
+
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
|
|
67
79
|
},
|
|
68
80
|
exitCode: 1,
|
|
69
81
|
});
|
|
@@ -72,6 +84,7 @@ path, options = {}) {
|
|
|
72
84
|
details: {
|
|
73
85
|
api_url: apiUrl,
|
|
74
86
|
reason: error instanceof Error ? error.message : "unknown",
|
|
87
|
+
...(idempotencyKey ? { idempotency_key: idempotencyKey } : {}),
|
|
75
88
|
},
|
|
76
89
|
exitCode: 1,
|
|
77
90
|
});
|
package/dist/index.js
CHANGED
|
@@ -7,14 +7,16 @@ import { createInterface } from "node:readline/promises";
|
|
|
7
7
|
import { stdin as input, stdout as output } from "node:process";
|
|
8
8
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
9
9
|
import { Command, Option } from "commander";
|
|
10
|
-
import {
|
|
10
|
+
import { applyOxygenHelp } from "./help.js";
|
|
11
|
+
import { buildCommandManifest } from "./command-manifest.js";
|
|
12
|
+
import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, formatCellForDisplay, formatPublicBudgetScopes, exitCodeForOxygenError, isVersionGreater, isVersionLess, OXYGEN_VERSION, OxygenError, parseKnowledgePageMarkdown, success, toFailure, } from "@oxygen/shared";
|
|
11
13
|
import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, } from "@oxygen/shared/file-import";
|
|
12
14
|
import { assertRecipeBundleSafe, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowManifest, } from "@oxygen/workflows";
|
|
13
15
|
import { isRecipeDefinition } from "@oxygen/recipe-sdk";
|
|
14
16
|
import { createBrowserLoginSession, openBrowser } from "./browser-login.js";
|
|
15
17
|
import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
|
|
16
18
|
import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
|
|
17
|
-
import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, markMirrorStale, mirrorExists, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
|
|
19
|
+
import { acquireMirrorLock, clearConflictFiles, deletePageFile, emptyMirrorState, findMirrorSlugByPageId, isFileDirty, listConflictFiles, localPageSha256, markMirrorStale, mirrorExists, pageFilePath, planMirrorPush, purgeMirror, resetMirrorForFullResync, quarantineDirtyFile, readMirrorState, releaseMirrorLock, resolveDefaultConfigDir, resolveMirrorDir, writeGeneratedIndexFile, writeGeneratedLogFile, writeMirrorState, writePageFile, } from "./knowledge-mirror.js";
|
|
18
20
|
import { waitForCliRun } from "./run-wait.js";
|
|
19
21
|
import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
|
|
20
22
|
import { captureCurrentTranscript, collectFeedbackEnvironment, TranscriptCaptureError, } from "./transcript.js";
|
|
@@ -119,12 +121,35 @@ async function handleAsyncAction(command, options, action) {
|
|
|
119
121
|
try {
|
|
120
122
|
const data = await action();
|
|
121
123
|
emitSuccess(command, data, options);
|
|
124
|
+
writeCreditsReceipt(data);
|
|
122
125
|
}
|
|
123
126
|
catch (error) {
|
|
124
127
|
const failure = toFailure(command, error);
|
|
125
128
|
writeJson(failure);
|
|
126
129
|
writeMaxCreditsHint(error);
|
|
127
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
130
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
// Paid envelopes (push 3 legibility) carry a `credits` block: quote on
|
|
134
|
+
// dry_run, receipt on live, remaining balance on both. Mirror it as one
|
|
135
|
+
// stderr line so spend stays visible in a terminal without polluting the
|
|
136
|
+
// machine-read stdout envelope.
|
|
137
|
+
function writeCreditsReceipt(data) {
|
|
138
|
+
if (!data || typeof data !== "object" || Array.isArray(data))
|
|
139
|
+
return;
|
|
140
|
+
const credits = data.credits;
|
|
141
|
+
if (!credits || typeof credits !== "object" || Array.isArray(credits))
|
|
142
|
+
return;
|
|
143
|
+
const block = credits;
|
|
144
|
+
const remaining = typeof block.credits_remaining === "number"
|
|
145
|
+
? block.credits_remaining.toLocaleString("en-US")
|
|
146
|
+
: null;
|
|
147
|
+
if (typeof block.credits_used === "number") {
|
|
148
|
+
process.stderr.write(`spent ${block.credits_used.toLocaleString("en-US")} credits${remaining !== null ? `, ${remaining} remaining` : ""}\n`);
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
if (typeof block.estimated_credits === "number") {
|
|
152
|
+
process.stderr.write(`estimated ${block.estimated_credits.toLocaleString("en-US")} credits for a live run${remaining !== null ? `, ${remaining} available` : ""}\n`);
|
|
128
153
|
}
|
|
129
154
|
}
|
|
130
155
|
// Paid live runs are refused server-side with typed spend-gate errors
|
|
@@ -1069,7 +1094,7 @@ export function createProgram() {
|
|
|
1069
1094
|
const binaryName = resolveCliBinaryName();
|
|
1070
1095
|
program
|
|
1071
1096
|
.name(binaryName)
|
|
1072
|
-
.description("
|
|
1097
|
+
.description("Revenue infrastructure for B2B startups — agent-operated GTM: tables, enrichment, sequences, workflows, CRM, knowledge. MCP + CLI first; every state-changing action returns a web_url deep-link.")
|
|
1073
1098
|
.version(OXYGEN_VERSION)
|
|
1074
1099
|
.option("--profile <name>", "Use a stored CLI profile for this command.")
|
|
1075
1100
|
.option("--org <organization>", "Use an organization id, Clerk org id, or slug for this command.");
|
|
@@ -1201,9 +1226,17 @@ export function createProgram() {
|
|
|
1201
1226
|
.command("whoami")
|
|
1202
1227
|
.description("Show the current Oxygen CLI identity.")
|
|
1203
1228
|
.option("--json", "Print a JSON envelope.")
|
|
1229
|
+
.option("--summary-json", "Print a compact identity envelope with no credential-shaped fields (safe for health probes and logs).")
|
|
1204
1230
|
.action(async (options) => {
|
|
1205
1231
|
await handleWhoamiAction(options);
|
|
1206
1232
|
});
|
|
1233
|
+
program
|
|
1234
|
+
.command("commands")
|
|
1235
|
+
.description("Print the machine-readable command manifest: every command with flags, spend/mutation markers, and the reserved exit-code table.")
|
|
1236
|
+
.option("--json", "Print a JSON envelope.")
|
|
1237
|
+
.action(async (options) => {
|
|
1238
|
+
await handleAsyncAction("commands", options, async () => buildCommandManifest(program, binaryName));
|
|
1239
|
+
});
|
|
1207
1240
|
program
|
|
1208
1241
|
.command("onboarding")
|
|
1209
1242
|
.description("Onboarding helpers.")
|
|
@@ -1239,14 +1272,26 @@ export function createProgram() {
|
|
|
1239
1272
|
requireAuth: false,
|
|
1240
1273
|
enforceMinimumCliVersion: false,
|
|
1241
1274
|
});
|
|
1275
|
+
// Explicit skew semantics: in_sync=false alone forced every ops sweep
|
|
1276
|
+
// to hand-interpret whether a version gap was safe (fleet friction,
|
|
1277
|
+
// recurring since 2026-06-18). `skew` names the direction and
|
|
1278
|
+
// `compatible` answers the only question that matters: is this CLI at
|
|
1279
|
+
// or above the server's enforced minimum?
|
|
1280
|
+
const minimum = server.minimum_cli_version ?? null;
|
|
1242
1281
|
return {
|
|
1243
1282
|
client_version: OXYGEN_VERSION,
|
|
1244
1283
|
api_url: apiUrl,
|
|
1245
1284
|
server_version: server.server_version,
|
|
1246
|
-
minimum_cli_version:
|
|
1285
|
+
minimum_cli_version: minimum,
|
|
1247
1286
|
sha: server.sha,
|
|
1248
1287
|
region: server.region,
|
|
1249
1288
|
in_sync: server.server_version === OXYGEN_VERSION,
|
|
1289
|
+
skew: isVersionGreater(OXYGEN_VERSION, server.server_version)
|
|
1290
|
+
? "client_ahead"
|
|
1291
|
+
: isVersionLess(OXYGEN_VERSION, server.server_version)
|
|
1292
|
+
? "client_behind"
|
|
1293
|
+
: "in_sync",
|
|
1294
|
+
compatible: minimum === null || !isVersionLess(OXYGEN_VERSION, minimum),
|
|
1250
1295
|
};
|
|
1251
1296
|
});
|
|
1252
1297
|
});
|
|
@@ -3160,6 +3205,28 @@ export function createProgram() {
|
|
|
3160
3205
|
method: "POST",
|
|
3161
3206
|
body: {},
|
|
3162
3207
|
}));
|
|
3208
|
+
}))
|
|
3209
|
+
.addCommand(new Command("synthesize")
|
|
3210
|
+
.description("Synthesize knowledge from workspace evidence (one low-cost AI call): campaign learnings from auto-filed outreach outcomes, or a voice guide from sent messages. Previews by default; --approved writes the result.")
|
|
3211
|
+
.option("--kind <kind>", "What to synthesize: campaign_learnings (default) or voice.", "campaign_learnings")
|
|
3212
|
+
.option("--sequence <sequence_id>", "Scope campaign learnings to one sequence's learnings page. Omit for the org-wide [[outreach-learnings]] page.")
|
|
3213
|
+
.option("--approved", "Write the synthesized result (learnings page revision / pinned voice guide) instead of previewing.")
|
|
3214
|
+
.option("--body <markdown>", "With --approved: save this edited body verbatim instead of re-synthesizing.")
|
|
3215
|
+
.option("--title <title>", "Optional title for a newly created page/asset.")
|
|
3216
|
+
.option("--limit <n>", "Voice only: how many recent sent messages to learn from (max 200).")
|
|
3217
|
+
.option("--json", "Print a JSON envelope.")
|
|
3218
|
+
.action(async (options) => {
|
|
3219
|
+
await handleAsyncAction("knowledge synthesize", options, () => requestOxygen("/api/cli/knowledge/synthesize", {
|
|
3220
|
+
method: "POST",
|
|
3221
|
+
body: {
|
|
3222
|
+
kind: options.kind ?? "campaign_learnings",
|
|
3223
|
+
...(readOption(options.sequence) ? { sequence: readOption(options.sequence) } : {}),
|
|
3224
|
+
...(options.approved ? { approved: true } : {}),
|
|
3225
|
+
...(readOption(options.body) ? { body: readOption(options.body) } : {}),
|
|
3226
|
+
...(readOption(options.title) ? { title: readOption(options.title) } : {}),
|
|
3227
|
+
...(readPositiveInt(options.limit) !== undefined ? { limit: readPositiveInt(options.limit) } : {}),
|
|
3228
|
+
},
|
|
3229
|
+
}));
|
|
3163
3230
|
}))
|
|
3164
3231
|
.addCommand(new Command("sync")
|
|
3165
3232
|
.description("Clone or refresh the local knowledge mirror: server-rendered page markdown under the CLI config dir.")
|
|
@@ -3169,6 +3236,13 @@ export function createProgram() {
|
|
|
3169
3236
|
.option("--json", "Print a JSON envelope.")
|
|
3170
3237
|
.action(async (options) => {
|
|
3171
3238
|
await handleAsyncAction("knowledge sync", options, () => runKnowledgeMirrorSync(options));
|
|
3239
|
+
}))
|
|
3240
|
+
.addCommand(new Command("push")
|
|
3241
|
+
.description("Push locally edited or newly created mirror pages back to the workspace wiki. Edits to canonical voice/brand/positioning pages become approval proposals instead of writing live.")
|
|
3242
|
+
.option("--dry-run", "List what would be pushed without writing anything.")
|
|
3243
|
+
.option("--json", "Print a JSON envelope.")
|
|
3244
|
+
.action(async (options) => {
|
|
3245
|
+
await handleAsyncAction("knowledge push", options, () => runKnowledgeMirrorPush(options));
|
|
3172
3246
|
}))
|
|
3173
3247
|
.addCommand(new Command("status")
|
|
3174
3248
|
.description("Report the local knowledge mirror: path, page count, last sync, quarantined conflicts.")
|
|
@@ -7282,6 +7356,95 @@ export function createProgram() {
|
|
|
7282
7356
|
});
|
|
7283
7357
|
});
|
|
7284
7358
|
})));
|
|
7359
|
+
program.addCommand(new Command("managed-inboxes")
|
|
7360
|
+
.description("Managed whitelabel sending inboxes on the OXYGEN-managed Cold Mail Reseller (CMR) account: subscribe a domain + N mailboxes (google/microsoft) as a recurring MONTHLY subscription billed to Oxygen credits, list/get your subscriptions, and cancel. Subscribe/cancel are approval-gated (preview → re-run with --approved --quote).")
|
|
7361
|
+
.addCommand(new Command("subscribe")
|
|
7362
|
+
.description("Subscribe a domain + mailboxes as a managed monthly CMR inbox subscription. WITHOUT --approved this prints a priced PREVIEW with a quote_id (nothing ordered or charged); re-run with --approved --quote <id> to place the order. Fails closed until the managed CMR wallet (CMR_RESELLER_API_KEY) + founder-signed pricing (CMR_MANAGED_INBOX_CREDITS_PER_MAILBOX) are configured.")
|
|
7363
|
+
.requiredOption("--domain <domain>", "Sending domain to register + host the mailboxes (e.g. send.acme.com).")
|
|
7364
|
+
.requiredOption("--provider <provider>", "Mailbox provider: google or microsoft.")
|
|
7365
|
+
.option("--mailboxes <json>", "JSON array of mailboxes: [{\"username\",\"first_name\",\"last_name\",\"profile_picture?\"}].")
|
|
7366
|
+
.option("--file <path>", "Path to a JSON file { \"mailboxes\": [...] } (alternative to --mailboxes).")
|
|
7367
|
+
.option("--years <n>", "Years to register the domain for (positive whole number). Defaults to 1.")
|
|
7368
|
+
.option("--billing <path>", "Path to a JSON file with the CMR billing address (required on the FIRST subscribe for an org that has no CMR account yet).")
|
|
7369
|
+
.option("--zapshield", "Add the Zapshield deliverability-protection add-on to the domain (a one-time per-domain add-on billed once its price is founder-signed; recorded but not charged until then).")
|
|
7370
|
+
.option("--approved", "Place the order (requires --quote). Without it, a priced preview is returned.")
|
|
7371
|
+
.option("--quote <id>", "The quote_id from a fresh preview. Required with --approved.")
|
|
7372
|
+
.option("--json", "Print a JSON envelope.")
|
|
7373
|
+
.action(async (options) => {
|
|
7374
|
+
await handleAsyncAction("managed-inboxes subscribe", options, () => {
|
|
7375
|
+
const domain = readOption(options.domain);
|
|
7376
|
+
const provider = readOption(options.provider);
|
|
7377
|
+
if (!domain)
|
|
7378
|
+
throw new Error("--domain is required.");
|
|
7379
|
+
if (!provider)
|
|
7380
|
+
throw new Error("--provider is required (google or microsoft).");
|
|
7381
|
+
let mailboxes;
|
|
7382
|
+
const mailboxesJson = readOption(options.mailboxes);
|
|
7383
|
+
const filePath = readOption(options.file);
|
|
7384
|
+
if (mailboxesJson) {
|
|
7385
|
+
mailboxes = JSON.parse(mailboxesJson);
|
|
7386
|
+
}
|
|
7387
|
+
else if (filePath) {
|
|
7388
|
+
const parsed = readJsonFileValue(resolve(filePath), "--file");
|
|
7389
|
+
mailboxes = parsed.mailboxes ?? [];
|
|
7390
|
+
}
|
|
7391
|
+
else {
|
|
7392
|
+
throw new Error("Provide --mailboxes <json> or --file <path>.");
|
|
7393
|
+
}
|
|
7394
|
+
const years = readOption(options.years);
|
|
7395
|
+
const billingPath = readOption(options.billing);
|
|
7396
|
+
const billing = billingPath ? readJsonFileValue(resolve(billingPath), "--billing") : undefined;
|
|
7397
|
+
const quote = readOption(options.quote);
|
|
7398
|
+
return requestOxygen("/api/cli/managed-inboxes/subscribe", {
|
|
7399
|
+
method: "POST",
|
|
7400
|
+
body: {
|
|
7401
|
+
domain,
|
|
7402
|
+
provider,
|
|
7403
|
+
mailboxes,
|
|
7404
|
+
...(years ? { years: Number(years) } : {}),
|
|
7405
|
+
...(billing ? { billing } : {}),
|
|
7406
|
+
...(options.zapshield ? { zapshield: true } : {}),
|
|
7407
|
+
...(options.approved ? { approved: true } : {}),
|
|
7408
|
+
...(quote ? { quote_id: quote } : {}),
|
|
7409
|
+
},
|
|
7410
|
+
});
|
|
7411
|
+
});
|
|
7412
|
+
}))
|
|
7413
|
+
.addCommand(new Command("list")
|
|
7414
|
+
.description("List the org's managed inbox subscriptions with provider, mailbox count, monthly price, CMR lifecycle status, and internal billing posture. Read-only, 0 Oxygen credits.")
|
|
7415
|
+
.option("--status <status>", "Filter by status: pending, active, renewing, past_due, expired, cancelled, or failed.")
|
|
7416
|
+
.option("--json", "Print a JSON envelope.")
|
|
7417
|
+
.action(async (options) => {
|
|
7418
|
+
await handleAsyncAction("managed-inboxes list", options, () => {
|
|
7419
|
+
const params = new URLSearchParams();
|
|
7420
|
+
const status = readOption(options.status);
|
|
7421
|
+
if (status)
|
|
7422
|
+
params.set("status", status);
|
|
7423
|
+
const suffix = params.toString();
|
|
7424
|
+
return requestOxygen(`/api/cli/managed-inboxes${suffix ? `?${suffix}` : ""}`);
|
|
7425
|
+
});
|
|
7426
|
+
}))
|
|
7427
|
+
.addCommand(new Command("get")
|
|
7428
|
+
.description("Get one managed inbox subscription by domain (provider, mailbox count, monthly price, CMR subscription id, lifecycle status, auto-renew, period end, internal billing status). Read-only, 0 Oxygen credits.")
|
|
7429
|
+
.argument("<domain>", "The managed inbox domain (e.g. send.acme.com).")
|
|
7430
|
+
.option("--json", "Print a JSON envelope.")
|
|
7431
|
+
.action(async (domain, options) => {
|
|
7432
|
+
await handleAsyncAction("managed-inboxes get", options, () => requestOxygen(`/api/cli/managed-inboxes/${encodeURIComponent(domain)}`));
|
|
7433
|
+
}))
|
|
7434
|
+
.addCommand(new Command("cancel")
|
|
7435
|
+
.description("Cancel a managed inbox subscription by domain (stops CMR's recurring monthly charge and frees the domain at period end). WITHOUT --approved prints a preview; re-run with --approved to cancel. Cancelling is free — 0 Oxygen credits.")
|
|
7436
|
+
.argument("<domain>", "The managed inbox domain to cancel.")
|
|
7437
|
+
.option("--approved", "Actually cancel (otherwise a preview is returned).")
|
|
7438
|
+
.option("--json", "Print a JSON envelope.")
|
|
7439
|
+
.action(async (domain, options) => {
|
|
7440
|
+
await handleAsyncAction("managed-inboxes cancel", options, () => requestOxygen("/api/cli/managed-inboxes/cancel", {
|
|
7441
|
+
method: "POST",
|
|
7442
|
+
body: {
|
|
7443
|
+
domain,
|
|
7444
|
+
...(options.approved ? { approved: true } : {}),
|
|
7445
|
+
},
|
|
7446
|
+
}));
|
|
7447
|
+
})));
|
|
7285
7448
|
program.addCommand(new Command("mailboxes")
|
|
7286
7449
|
.description("Native email sending pool: register/refresh Google/Microsoft mailboxes a campaign rotates over, pause/disable inboxes, and delegate warmup to Instantly (BYOK — Instantly bills your account, 0 Oxygen credits).")
|
|
7287
7450
|
.addCommand(new Command("list")
|
|
@@ -7528,6 +7691,28 @@ export function createProgram() {
|
|
|
7528
7691
|
const suffix = params.toString();
|
|
7529
7692
|
return requestOxygen(`/api/cli/mailboxes/warmup/status${suffix ? `?${suffix}` : ""}`);
|
|
7530
7693
|
});
|
|
7694
|
+
}))
|
|
7695
|
+
.addCommand(new Command("provision")
|
|
7696
|
+
.description("Export sending mailboxes into the Instantly workspace via Zapmail (the prerequisite for warmup), then let the worker enable warmup once the export completes. Targets the whole pool unless --mailboxes is given. Uses the org's Zapmail connection (BYOK) or the OXYGEN-managed wallet.")
|
|
7697
|
+
.option("--mailboxes <list>", "Comma-separated mailbox ids or addresses to provision. Omit for the whole pool.")
|
|
7698
|
+
.option("--connection <id>", "Zapmail connection id. Defaults to the org's active Zapmail connection (or the managed wallet).")
|
|
7699
|
+
.option("--force", "Re-export mailboxes already exporting/warming/active (default skips them).")
|
|
7700
|
+
.option("--dry-run", "Simulate without calling Zapmail (reports the mailboxes that would be exported).")
|
|
7701
|
+
.option("--json", "Print a JSON envelope.")
|
|
7702
|
+
.action(async (options) => {
|
|
7703
|
+
await handleAsyncAction("mailboxes warmup provision", options, () => {
|
|
7704
|
+
const mailboxes = readCsvOption(options.mailboxes);
|
|
7705
|
+
const connection = readOption(options.connection);
|
|
7706
|
+
return requestOxygen("/api/cli/mailboxes/warmup/provision", {
|
|
7707
|
+
method: "POST",
|
|
7708
|
+
body: {
|
|
7709
|
+
...(mailboxes.length > 0 ? { mailboxes } : {}),
|
|
7710
|
+
...(connection ? { connection_id: connection } : {}),
|
|
7711
|
+
...(options.force ? { force: true } : {}),
|
|
7712
|
+
...(options.dryRun ? { dry_run: true } : {}),
|
|
7713
|
+
},
|
|
7714
|
+
});
|
|
7715
|
+
});
|
|
7531
7716
|
})))
|
|
7532
7717
|
.addCommand(new Command("order")
|
|
7533
7718
|
.description("Order NEW sending mailboxes (domain + inboxes + warmup) on the OXYGEN-managed Zapmail wallet, billed to Oxygen credits. Without --approved returns a priced preview + quote_id. FAILS CLOSED (409) until managed ordering is enabled.")
|
|
@@ -8195,6 +8380,7 @@ export function createProgram() {
|
|
|
8195
8380
|
.action(async (options) => {
|
|
8196
8381
|
await handleAsyncAction("skills install", options, () => installAgentSkills(options));
|
|
8197
8382
|
}));
|
|
8383
|
+
applyOxygenHelp(program, binaryName);
|
|
8198
8384
|
return program;
|
|
8199
8385
|
}
|
|
8200
8386
|
/**
|
|
@@ -10772,6 +10958,27 @@ async function handleWhoamiAction(options) {
|
|
|
10772
10958
|
enforceMinimumCliVersion: false,
|
|
10773
10959
|
});
|
|
10774
10960
|
const context = await resolveActiveProfileWithSource();
|
|
10961
|
+
// Compact, credential-free identity shape for health probes and ops
|
|
10962
|
+
// sweeps: the full envelope carries apiKey token prefix/suffix metadata,
|
|
10963
|
+
// which forced every automated consumer to sanitize before logging
|
|
10964
|
+
// (recurring fleet friction since 2026-06-18).
|
|
10965
|
+
if (options.summaryJson) {
|
|
10966
|
+
writeJson(success("whoami", {
|
|
10967
|
+
user: { id: identity.user.id, email: identity.user.email },
|
|
10968
|
+
organization: {
|
|
10969
|
+
id: identity.organization.id,
|
|
10970
|
+
name: identity.organization.name,
|
|
10971
|
+
slug: identity.organization.slug,
|
|
10972
|
+
},
|
|
10973
|
+
auth_type: identity.authType ?? identity.apiKey?.kind ?? null,
|
|
10974
|
+
onboarding_complete: identity.onboarding?.complete ?? null,
|
|
10975
|
+
profile: context.resolution.exists ? context.resolution.name : null,
|
|
10976
|
+
profile_source: context.source,
|
|
10977
|
+
api_url: context.resolution.credentials?.apiUrl ?? null,
|
|
10978
|
+
cli_version: OXYGEN_VERSION,
|
|
10979
|
+
}));
|
|
10980
|
+
return;
|
|
10981
|
+
}
|
|
10775
10982
|
if (context.resolution.exists) {
|
|
10776
10983
|
const existingCredentials = context.resolution.credentials;
|
|
10777
10984
|
const cached = existingCredentials?.identity;
|
|
@@ -10807,7 +11014,7 @@ async function handleWhoamiAction(options) {
|
|
|
10807
11014
|
catch (error) {
|
|
10808
11015
|
const failure = toFailure("whoami", error);
|
|
10809
11016
|
writeJson(failure);
|
|
10810
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11017
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
10811
11018
|
}
|
|
10812
11019
|
}
|
|
10813
11020
|
async function handleOnboardingStartAction(options) {
|
|
@@ -10822,7 +11029,7 @@ async function handleOnboardingStartAction(options) {
|
|
|
10822
11029
|
catch (error) {
|
|
10823
11030
|
const failure = toFailure("onboarding start", error);
|
|
10824
11031
|
writeJson(failure);
|
|
10825
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11032
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
10826
11033
|
}
|
|
10827
11034
|
}
|
|
10828
11035
|
async function handleOnboardingResetAction(options) {
|
|
@@ -10846,7 +11053,7 @@ async function handleOnboardingResetAction(options) {
|
|
|
10846
11053
|
catch (error) {
|
|
10847
11054
|
const failure = toFailure("onboarding reset", error);
|
|
10848
11055
|
writeJson(failure);
|
|
10849
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11056
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
10850
11057
|
}
|
|
10851
11058
|
}
|
|
10852
11059
|
function formatOnboardingReset(data) {
|
|
@@ -10904,7 +11111,7 @@ async function handleLoginAction(options) {
|
|
|
10904
11111
|
catch (error) {
|
|
10905
11112
|
const failure = toFailure("login", error);
|
|
10906
11113
|
writeJson(failure);
|
|
10907
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11114
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
10908
11115
|
}
|
|
10909
11116
|
}
|
|
10910
11117
|
async function handleAuthUseTokenAction(options) {
|
|
@@ -10934,7 +11141,7 @@ async function handleAuthUseTokenAction(options) {
|
|
|
10934
11141
|
writeJson(failure);
|
|
10935
11142
|
else
|
|
10936
11143
|
process.stderr.write(`${failure.error.message}\n`);
|
|
10937
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11144
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
10938
11145
|
}
|
|
10939
11146
|
}
|
|
10940
11147
|
async function handleAuthDoctorAction(options) {
|
|
@@ -10949,7 +11156,7 @@ async function handleAuthDoctorAction(options) {
|
|
|
10949
11156
|
catch (error) {
|
|
10950
11157
|
const failure = toFailure("auth doctor", error);
|
|
10951
11158
|
writeJson(failure);
|
|
10952
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11159
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
10953
11160
|
}
|
|
10954
11161
|
}
|
|
10955
11162
|
async function runAuthDoctor() {
|
|
@@ -11040,7 +11247,7 @@ async function handleOrgUseAction(organization, options, command) {
|
|
|
11040
11247
|
catch (error) {
|
|
11041
11248
|
const failure = toFailure(command, error);
|
|
11042
11249
|
writeJson(failure);
|
|
11043
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11250
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11044
11251
|
}
|
|
11045
11252
|
}
|
|
11046
11253
|
async function handleProfilesListAction(options) {
|
|
@@ -11059,7 +11266,7 @@ async function handleProfilesListAction(options) {
|
|
|
11059
11266
|
catch (error) {
|
|
11060
11267
|
const failure = toFailure("profiles list", error);
|
|
11061
11268
|
writeJson(failure);
|
|
11062
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11269
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11063
11270
|
}
|
|
11064
11271
|
}
|
|
11065
11272
|
async function handleProfilesUseAction(profile, options) {
|
|
@@ -11080,7 +11287,7 @@ async function handleProfilesUseAction(profile, options) {
|
|
|
11080
11287
|
catch (error) {
|
|
11081
11288
|
const failure = toFailure("profiles use", error);
|
|
11082
11289
|
writeJson(failure);
|
|
11083
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11290
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11084
11291
|
}
|
|
11085
11292
|
}
|
|
11086
11293
|
async function handleProfilesEnvAction(profile, options) {
|
|
@@ -11119,7 +11326,7 @@ async function handleProfilesEnvAction(profile, options) {
|
|
|
11119
11326
|
catch (error) {
|
|
11120
11327
|
const failure = toFailure("profiles env", error);
|
|
11121
11328
|
writeJson(failure);
|
|
11122
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11329
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11123
11330
|
}
|
|
11124
11331
|
}
|
|
11125
11332
|
async function handleProfilesCurrentAction(options) {
|
|
@@ -11143,7 +11350,7 @@ async function handleProfilesCurrentAction(options) {
|
|
|
11143
11350
|
catch (error) {
|
|
11144
11351
|
const failure = toFailure("profiles current", error);
|
|
11145
11352
|
writeJson(failure);
|
|
11146
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11353
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11147
11354
|
}
|
|
11148
11355
|
}
|
|
11149
11356
|
function shellQuote(value) {
|
|
@@ -11172,7 +11379,7 @@ async function handleLogoutAction(options) {
|
|
|
11172
11379
|
catch (error) {
|
|
11173
11380
|
const failure = toFailure("logout", error);
|
|
11174
11381
|
writeJson(failure);
|
|
11175
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11382
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11176
11383
|
}
|
|
11177
11384
|
}
|
|
11178
11385
|
async function handleUpdateAction(options) {
|
|
@@ -11187,7 +11394,7 @@ async function handleUpdateAction(options) {
|
|
|
11187
11394
|
catch (error) {
|
|
11188
11395
|
const failure = toFailure("update", error);
|
|
11189
11396
|
writeJson(failure);
|
|
11190
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
11397
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11191
11398
|
}
|
|
11192
11399
|
}
|
|
11193
11400
|
function buildApiKeyCreateBody(options) {
|
|
@@ -11798,7 +12005,7 @@ async function handleSequenceSignalAction(sequence, options) {
|
|
|
11798
12005
|
const failure = toFailure("sequences signal", error);
|
|
11799
12006
|
writeJson(failure);
|
|
11800
12007
|
writeMaxCreditsHint(error);
|
|
11801
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
12008
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11802
12009
|
}
|
|
11803
12010
|
}
|
|
11804
12011
|
async function handleSequenceVariantsAction(sequence, options) {
|
|
@@ -11835,7 +12042,7 @@ async function handleSequenceVariantsAction(sequence, options) {
|
|
|
11835
12042
|
const failure = toFailure("sequences variants", error);
|
|
11836
12043
|
writeJson(failure);
|
|
11837
12044
|
writeMaxCreditsHint(error);
|
|
11838
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
12045
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11839
12046
|
}
|
|
11840
12047
|
}
|
|
11841
12048
|
function formatVariantCell(value) {
|
|
@@ -11966,7 +12173,7 @@ async function handleTablesTidySuggestAction(table, options) {
|
|
|
11966
12173
|
const failure = toFailure("tables tidy-suggest", error);
|
|
11967
12174
|
writeJson(failure);
|
|
11968
12175
|
writeMaxCreditsHint(error);
|
|
11969
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
12176
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
11970
12177
|
}
|
|
11971
12178
|
}
|
|
11972
12179
|
function formatTidyCount(value) {
|
|
@@ -12349,7 +12556,7 @@ async function handleSupportAdminWorkflowAction(ticketId, options) {
|
|
|
12349
12556
|
catch (error) {
|
|
12350
12557
|
const failure = toFailure("support admin workflow", error);
|
|
12351
12558
|
writeJson(failure);
|
|
12352
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
12559
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
12353
12560
|
}
|
|
12354
12561
|
}
|
|
12355
12562
|
async function handleSupportAdminUpdateAction(ticketId, options) {
|
|
@@ -12365,7 +12572,7 @@ async function handleSupportAdminUpdateAction(ticketId, options) {
|
|
|
12365
12572
|
catch (error) {
|
|
12366
12573
|
const failure = toFailure("support admin update", error);
|
|
12367
12574
|
writeJson(failure);
|
|
12368
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
12575
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
12369
12576
|
}
|
|
12370
12577
|
}
|
|
12371
12578
|
function formatSupportAdminWorkflowStep(styles, label, status, artifactLabel, artifact) {
|
|
@@ -12916,6 +13123,152 @@ async function runKnowledgeMirrorSync(options) {
|
|
|
12916
13123
|
releaseMirrorLock(target.dir);
|
|
12917
13124
|
}
|
|
12918
13125
|
}
|
|
13126
|
+
// One page's push: parse the local file, guard identity, upsert with the manifest
|
|
13127
|
+
// revision as the optimistic-concurrency base, and translate the two expected
|
|
13128
|
+
// gate outcomes (approval proposal, revision conflict) into statuses.
|
|
13129
|
+
async function pushOneMirrorPage(dir, state, candidate) {
|
|
13130
|
+
const { slug, kind, record } = candidate;
|
|
13131
|
+
let contents;
|
|
13132
|
+
try {
|
|
13133
|
+
contents = readFileSync(pageFilePath(dir, slug), "utf8");
|
|
13134
|
+
}
|
|
13135
|
+
catch {
|
|
13136
|
+
return { slug, kind, status: "skipped", reason: "file_unreadable" };
|
|
13137
|
+
}
|
|
13138
|
+
const parsed = parseKnowledgePageMarkdown(contents);
|
|
13139
|
+
if (record && parsed.id && parsed.id !== record.id) {
|
|
13140
|
+
return { slug, kind, status: "skipped", reason: "id_mismatch_with_manifest" };
|
|
13141
|
+
}
|
|
13142
|
+
if (parsed.slug && parsed.slug !== slug) {
|
|
13143
|
+
return { slug, kind, status: "skipped", reason: "frontmatter_slug_differs_from_filename" };
|
|
13144
|
+
}
|
|
13145
|
+
const body = {
|
|
13146
|
+
slug,
|
|
13147
|
+
body: parsed.body,
|
|
13148
|
+
...(parsed.title ? { title: parsed.title } : {}),
|
|
13149
|
+
...(parsed.type ? { type: parsed.type } : {}),
|
|
13150
|
+
...(parsed.status ? { status: parsed.status } : {}),
|
|
13151
|
+
...(parsed.tags ? { tags: parsed.tags } : {}),
|
|
13152
|
+
// A frontmatter block owns the summary line: pushing an emptied summary
|
|
13153
|
+
// clears it server-side; a plain new file leaves summary untouched.
|
|
13154
|
+
...(parsed.hasFrontmatter ? { summary: parsed.summary } : {}),
|
|
13155
|
+
...(record ? { expected_revision: record.revision } : {}),
|
|
13156
|
+
};
|
|
13157
|
+
try {
|
|
13158
|
+
const data = await requestOxygen("/api/cli/knowledge/pages/upsert", { method: "POST", body });
|
|
13159
|
+
const page = data.page ?? {};
|
|
13160
|
+
if (typeof page.id === "string" && typeof page.revision === "number") {
|
|
13161
|
+
// Record the pushed bytes as the manifest truth so the follow-up sync
|
|
13162
|
+
// overwrites this file with the server rendering instead of quarantining
|
|
13163
|
+
// the very edit we just pushed.
|
|
13164
|
+
const localSha = localPageSha256(dir, slug);
|
|
13165
|
+
if (localSha) {
|
|
13166
|
+
state.pages[slug] = { id: page.id, revision: page.revision, content_sha256: localSha };
|
|
13167
|
+
}
|
|
13168
|
+
}
|
|
13169
|
+
return {
|
|
13170
|
+
slug,
|
|
13171
|
+
kind,
|
|
13172
|
+
status: "pushed",
|
|
13173
|
+
...(typeof page.revision === "number" ? { revision: page.revision } : {}),
|
|
13174
|
+
};
|
|
13175
|
+
}
|
|
13176
|
+
catch (error) {
|
|
13177
|
+
if (error instanceof OxygenError && error.code === "knowledge_approval_required") {
|
|
13178
|
+
const details = (error.details ?? {});
|
|
13179
|
+
return {
|
|
13180
|
+
slug,
|
|
13181
|
+
kind,
|
|
13182
|
+
status: "proposal_created",
|
|
13183
|
+
...(typeof details.proposalId === "string" ? { proposal_id: details.proposalId } : {}),
|
|
13184
|
+
...(typeof details.proposalSlug === "string" ? { proposal_slug: details.proposalSlug } : {}),
|
|
13185
|
+
reason: typeof details.reason === "string" ? details.reason : "sensitive_page",
|
|
13186
|
+
};
|
|
13187
|
+
}
|
|
13188
|
+
if (error instanceof OxygenError && error.code === "knowledge_page_conflict") {
|
|
13189
|
+
return { slug, kind, status: "conflict", reason: "revision_moved_run_knowledge_sync_first" };
|
|
13190
|
+
}
|
|
13191
|
+
if (error instanceof OxygenError) {
|
|
13192
|
+
return { slug, kind, status: "failed", reason: error.code };
|
|
13193
|
+
}
|
|
13194
|
+
throw error;
|
|
13195
|
+
}
|
|
13196
|
+
}
|
|
13197
|
+
// `oxygen knowledge push` — the write half of the local mirror: locally edited
|
|
13198
|
+
// (dirty) and brand-new mirror files upsert back to the workspace wiki through
|
|
13199
|
+
// the same gate as every other write surface, then a delta sync converges the
|
|
13200
|
+
// mirror onto the server rendering. This is what lets a local agent (Claude
|
|
13201
|
+
// Code, Codex) work the wiki as plain markdown files.
|
|
13202
|
+
async function runKnowledgeMirrorPush(options) {
|
|
13203
|
+
const target = await resolveKnowledgeMirrorTarget();
|
|
13204
|
+
const state = readMirrorState(target.dir);
|
|
13205
|
+
if (!state) {
|
|
13206
|
+
throw new OxygenError("knowledge_mirror_missing", "No local knowledge mirror exists for this organization yet. Run `oxygen knowledge sync` first.", {
|
|
13207
|
+
details: { mirror_path: target.dir },
|
|
13208
|
+
exitCode: 1,
|
|
13209
|
+
});
|
|
13210
|
+
}
|
|
13211
|
+
const plan = planMirrorPush(target.dir, state);
|
|
13212
|
+
if (options.dryRun) {
|
|
13213
|
+
return {
|
|
13214
|
+
dry_run: true,
|
|
13215
|
+
candidates: plan.candidates.map(({ slug, kind, record }) => ({
|
|
13216
|
+
slug,
|
|
13217
|
+
kind,
|
|
13218
|
+
...(record ? { base_revision: record.revision } : {}),
|
|
13219
|
+
})),
|
|
13220
|
+
skipped: plan.skipped,
|
|
13221
|
+
mirror_path: target.dir,
|
|
13222
|
+
web_url: target.webUrl,
|
|
13223
|
+
};
|
|
13224
|
+
}
|
|
13225
|
+
// Hold the mirror lock through the push phase so a concurrent sync can't
|
|
13226
|
+
// rewrite files or the manifest mid-push; released before the convergence
|
|
13227
|
+
// sync below, which takes the lock itself.
|
|
13228
|
+
if (!acquireMirrorLock(target.dir)) {
|
|
13229
|
+
throw new OxygenError("knowledge_sync_locked", "Another knowledge sync is running for this mirror. Retry shortly; a crashed sync's lock self-clears after 10 minutes.", {
|
|
13230
|
+
details: { mirror_path: target.dir },
|
|
13231
|
+
exitCode: 1,
|
|
13232
|
+
});
|
|
13233
|
+
}
|
|
13234
|
+
const outcomes = [];
|
|
13235
|
+
let pushedCount = 0;
|
|
13236
|
+
try {
|
|
13237
|
+
for (const candidate of plan.candidates) {
|
|
13238
|
+
outcomes.push(await pushOneMirrorPage(target.dir, state, candidate));
|
|
13239
|
+
}
|
|
13240
|
+
pushedCount = outcomes.filter((outcome) => outcome.status === "pushed").length;
|
|
13241
|
+
if (pushedCount > 0) {
|
|
13242
|
+
writeMirrorState(target.dir, state);
|
|
13243
|
+
}
|
|
13244
|
+
}
|
|
13245
|
+
finally {
|
|
13246
|
+
releaseMirrorLock(target.dir);
|
|
13247
|
+
}
|
|
13248
|
+
// Converge: pull the server's rendering of everything just pushed (plus any
|
|
13249
|
+
// proposal draft pages the gate filed) so local bytes match the server again.
|
|
13250
|
+
let syncedAfter = false;
|
|
13251
|
+
if (pushedCount > 0 || outcomes.some((outcome) => outcome.status === "proposal_created")) {
|
|
13252
|
+
await runKnowledgeMirrorSync({});
|
|
13253
|
+
syncedAfter = true;
|
|
13254
|
+
}
|
|
13255
|
+
return {
|
|
13256
|
+
pushed: pushedCount,
|
|
13257
|
+
proposals: outcomes.filter((outcome) => outcome.status === "proposal_created"),
|
|
13258
|
+
conflicts: outcomes.filter((outcome) => outcome.status === "conflict"),
|
|
13259
|
+
failures: outcomes.filter((outcome) => outcome.status === "failed"),
|
|
13260
|
+
skipped: [
|
|
13261
|
+
...plan.skipped,
|
|
13262
|
+
...outcomes
|
|
13263
|
+
.filter((outcome) => outcome.status === "skipped")
|
|
13264
|
+
.map(({ slug, reason }) => ({ file: `${slug}.md`, reason: reason ?? "skipped" })),
|
|
13265
|
+
],
|
|
13266
|
+
outcomes,
|
|
13267
|
+
synced_after: syncedAfter,
|
|
13268
|
+
mirror_path: target.dir,
|
|
13269
|
+
web_url: target.webUrl,
|
|
13270
|
+
};
|
|
13271
|
+
}
|
|
12919
13272
|
async function runKnowledgeMirrorStatus(options) {
|
|
12920
13273
|
const target = await resolveKnowledgeMirrorTarget();
|
|
12921
13274
|
const clearedConflicts = options.clearConflicts ? clearConflictFiles(target.dir) : undefined;
|
|
@@ -12949,7 +13302,7 @@ async function handleKnowledgeMirrorPrintPathAction() {
|
|
|
12949
13302
|
catch (error) {
|
|
12950
13303
|
const failure = toFailure("knowledge status", error);
|
|
12951
13304
|
writeJson(failure);
|
|
12952
|
-
process.exitCode = error instanceof OxygenError ? error
|
|
13305
|
+
process.exitCode = error instanceof OxygenError ? exitCodeForOxygenError(error) : 1;
|
|
12953
13306
|
}
|
|
12954
13307
|
}
|
|
12955
13308
|
// Knowledge page routes accept either a UUID (`id`) or a slug (`slug`); split
|
|
@@ -13194,4 +13547,13 @@ function muteTokenEcho() {
|
|
|
13194
13547
|
return () => undefined;
|
|
13195
13548
|
}
|
|
13196
13549
|
}
|
|
13550
|
+
// Piping CLI output into a consumer that exits early (`oxygen whoami --json |
|
|
13551
|
+
// head -c 20`) otherwise crashes with a raw EPIPE stack after the consumer
|
|
13552
|
+
// closes the pipe; a closed pipe is normal shell behavior, not an error.
|
|
13553
|
+
process.stdout.on("error", (error) => {
|
|
13554
|
+
if (error.code === "EPIPE")
|
|
13555
|
+
process.exit(0);
|
|
13556
|
+
process.stderr.write(`stdout error: ${error.message}\n`);
|
|
13557
|
+
process.exit(1);
|
|
13558
|
+
});
|
|
13197
13559
|
await createProgram().parseAsync(process.argv);
|
|
@@ -92,6 +92,28 @@ export declare function acquireMirrorLock(dir: string, options?: {
|
|
|
92
92
|
export declare function releaseMirrorLock(dir: string): void;
|
|
93
93
|
export declare function writeGeneratedIndexFile(dir: string, index: MirrorIndexSnapshot): void;
|
|
94
94
|
export declare function writeGeneratedLogFile(dir: string, entries: MirrorLogEntry[]): void;
|
|
95
|
+
export type MirrorPushCandidate = {
|
|
96
|
+
slug: string;
|
|
97
|
+
/** dirty = tracked page whose local bytes drifted; new = untracked local file. */
|
|
98
|
+
kind: "dirty" | "new";
|
|
99
|
+
path: string;
|
|
100
|
+
/** Manifest record for dirty pages — its revision is the optimistic-concurrency base. */
|
|
101
|
+
record?: MirrorPageRecord;
|
|
102
|
+
};
|
|
103
|
+
export type MirrorPushPlan = {
|
|
104
|
+
candidates: MirrorPushCandidate[];
|
|
105
|
+
skipped: Array<{
|
|
106
|
+
file: string;
|
|
107
|
+
reason: "invalid_slug" | "reserved_slug" | "generated_file";
|
|
108
|
+
}>;
|
|
109
|
+
};
|
|
110
|
+
/**
|
|
111
|
+
* Scan the mirror for pushable local state: tracked pages whose bytes drifted
|
|
112
|
+
* from the manifest sha (edited locally) and untracked `.md` files (created
|
|
113
|
+
* locally). Generated index/log summaries and reserved slugs are skipped.
|
|
114
|
+
* Read-only — the push command decides what to do with the plan.
|
|
115
|
+
*/
|
|
116
|
+
export declare function planMirrorPush(dir: string, state: MirrorState | null): MirrorPushPlan;
|
|
95
117
|
/** Delete the whole mirror (pages, generated files, sidecar). Safe on a missing dir. */
|
|
96
118
|
export declare function purgeMirror(dir: string): void;
|
|
97
119
|
/**
|
package/dist/knowledge-mirror.js
CHANGED
|
@@ -327,6 +327,62 @@ function writeGeneratedFile(dir, name, lines) {
|
|
|
327
327
|
function oneLine(value) {
|
|
328
328
|
return value.replace(/\s+/g, " ").trim();
|
|
329
329
|
}
|
|
330
|
+
// Slugs the server write path refuses (RESERVED_KNOWLEDGE_SLUGS) — an untracked
|
|
331
|
+
// local file under one of these names can never push. A TRACKED page under one
|
|
332
|
+
// (legacy rows may predate the reservation) still pushes by id.
|
|
333
|
+
const PUSH_RESERVED_SLUGS = new Set(["index", "log", "schema", "company-profile", "readme"]);
|
|
334
|
+
/**
|
|
335
|
+
* Scan the mirror for pushable local state: tracked pages whose bytes drifted
|
|
336
|
+
* from the manifest sha (edited locally) and untracked `.md` files (created
|
|
337
|
+
* locally). Generated index/log summaries and reserved slugs are skipped.
|
|
338
|
+
* Read-only — the push command decides what to do with the plan.
|
|
339
|
+
*/
|
|
340
|
+
export function planMirrorPush(dir, state) {
|
|
341
|
+
const plan = { candidates: [], skipped: [] };
|
|
342
|
+
let entries = [];
|
|
343
|
+
try {
|
|
344
|
+
entries = readdirSync(dir);
|
|
345
|
+
}
|
|
346
|
+
catch {
|
|
347
|
+
return plan; // no mirror directory — nothing to push
|
|
348
|
+
}
|
|
349
|
+
for (const entry of entries.sort((a, b) => a.localeCompare(b))) {
|
|
350
|
+
if (!entry.endsWith(".md"))
|
|
351
|
+
continue;
|
|
352
|
+
const slug = entry.slice(0, -3);
|
|
353
|
+
const path = join(dir, entry);
|
|
354
|
+
const record = state?.pages[slug];
|
|
355
|
+
if (record) {
|
|
356
|
+
if (isFileDirty(dir, slug, record.content_sha256)) {
|
|
357
|
+
plan.candidates.push({ slug, kind: "dirty", path, record });
|
|
358
|
+
}
|
|
359
|
+
continue;
|
|
360
|
+
}
|
|
361
|
+
if (slug.length > MIRROR_SLUG_MAX_LENGTH || !MIRROR_SLUG_PATTERN.test(slug)) {
|
|
362
|
+
plan.skipped.push({ file: entry, reason: "invalid_slug" });
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
// Untracked generated summaries (index.md / log.md before their slugs are
|
|
366
|
+
// ever page-backed) carry the marker header — never push those.
|
|
367
|
+
let head = "";
|
|
368
|
+
try {
|
|
369
|
+
head = readFileSync(path, "utf8").slice(0, GENERATED_MIRROR_FILE_HEADER.length);
|
|
370
|
+
}
|
|
371
|
+
catch {
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
if (head === GENERATED_MIRROR_FILE_HEADER) {
|
|
375
|
+
plan.skipped.push({ file: entry, reason: "generated_file" });
|
|
376
|
+
continue;
|
|
377
|
+
}
|
|
378
|
+
if (PUSH_RESERVED_SLUGS.has(slug)) {
|
|
379
|
+
plan.skipped.push({ file: entry, reason: "reserved_slug" });
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
plan.candidates.push({ slug, kind: "new", path });
|
|
383
|
+
}
|
|
384
|
+
return plan;
|
|
385
|
+
}
|
|
330
386
|
// ── Purge ───────────────────────────────────────────────────────────────────────
|
|
331
387
|
/** Delete the whole mirror (pages, generated files, sidecar). Safe on a missing dir. */
|
|
332
388
|
export function purgeMirror(dir) {
|
|
@@ -26,6 +26,8 @@ export type PricingPlanDefinition = {
|
|
|
26
26
|
};
|
|
27
27
|
export type PricingPlanMetadata = Record<string, unknown> | null | undefined;
|
|
28
28
|
export declare const CONTACT_SALES_URL = "https://cal.com/tim-scheuer-mxbib9/45";
|
|
29
|
+
export declare const TRIAL_PERIOD_DAYS = 7;
|
|
30
|
+
export declare const TRIAL_CREDIT_GRANT = 1000;
|
|
29
31
|
export declare const BASE_PRICING_PLANS: {
|
|
30
32
|
readonly free: {
|
|
31
33
|
readonly tier: "free";
|
|
@@ -2,6 +2,12 @@ export const WEEKLY_USAGE_WINDOW_DAYS = 7;
|
|
|
2
2
|
export const BILLING_CURRENCIES = ["usd"];
|
|
3
3
|
export const DEFAULT_BILLING_CURRENCY = "usd";
|
|
4
4
|
export const CONTACT_SALES_URL = "https://cal.com/tim-scheuer-mxbib9/45";
|
|
5
|
+
// Card-required free trial: every new signup starts a Stripe trial on the Starter
|
|
6
|
+
// plan, gets TRIAL_CREDIT_GRANT credits (a value-demonstration budget sized to the
|
|
7
|
+
// guided first outcome — NOT the full plan allowance), and converts to paid on
|
|
8
|
+
// credit exhaustion or when the trial clock elapses, whichever comes first.
|
|
9
|
+
export const TRIAL_PERIOD_DAYS = 7;
|
|
10
|
+
export const TRIAL_CREDIT_GRANT = 1_000;
|
|
5
11
|
export const BASE_PRICING_PLANS = {
|
|
6
12
|
free: {
|
|
7
13
|
tier: "free",
|
|
@@ -30,6 +30,9 @@ export declare class OxygenError extends Error {
|
|
|
30
30
|
exitCode?: number;
|
|
31
31
|
});
|
|
32
32
|
}
|
|
33
|
+
export declare const CLI_EXIT_CODE_TABLE: Readonly<Record<number, string>>;
|
|
34
|
+
export declare function exitCodeForErrorCode(code: string): number;
|
|
35
|
+
export declare function exitCodeForOxygenError(error: OxygenError): number;
|
|
33
36
|
export declare function success<T>(command: string, data: T, version?: string, minimumCliVersion?: string): CliSuccess<T>;
|
|
34
37
|
export declare function failure(command: string, error: {
|
|
35
38
|
code: string;
|
|
@@ -19,6 +19,71 @@ export class OxygenError extends Error {
|
|
|
19
19
|
this.exitCode = options.exitCode ?? 1;
|
|
20
20
|
}
|
|
21
21
|
}
|
|
22
|
+
// Reserved CLI exit codes, so shell agents can branch on failure class without
|
|
23
|
+
// parsing the JSON envelope. Documented in `oxygen --help` and in the
|
|
24
|
+
// `oxygen commands --json` manifest; treat as a stable contract.
|
|
25
|
+
//
|
|
26
|
+
// 0 ok · 1 unclassified error · 2 usage / input validation · 3 auth or CLI
|
|
27
|
+
// compatibility (not_authenticated, cli_update_required) · 4 not found ·
|
|
28
|
+
// 5 provider / server failure · 6 rate limit (details.retry_after_seconds
|
|
29
|
+
// says how long to wait) · 7 approval / spend gate (re-run with --approved
|
|
30
|
+
// --max-credits <n>) · 8 timeout.
|
|
31
|
+
export const CLI_EXIT_CODE_TABLE = {
|
|
32
|
+
0: "ok",
|
|
33
|
+
1: "unclassified error",
|
|
34
|
+
2: "usage / input validation",
|
|
35
|
+
3: "auth or CLI compatibility",
|
|
36
|
+
4: "not found",
|
|
37
|
+
5: "provider or server failure",
|
|
38
|
+
6: "rate limited (wait details.retry_after_seconds, then retry)",
|
|
39
|
+
7: "approval / spend gate (re-run with --approved --max-credits <n>)",
|
|
40
|
+
8: "timeout",
|
|
41
|
+
};
|
|
42
|
+
export function exitCodeForErrorCode(code) {
|
|
43
|
+
switch (code) {
|
|
44
|
+
case "not_authenticated":
|
|
45
|
+
case "unauthorized":
|
|
46
|
+
case "invalid_token":
|
|
47
|
+
case "cli_update_required":
|
|
48
|
+
return 3;
|
|
49
|
+
case "rate_limit_exceeded":
|
|
50
|
+
return 6;
|
|
51
|
+
case "max_credits_required":
|
|
52
|
+
case "approval_required":
|
|
53
|
+
case "spend_cap_required":
|
|
54
|
+
case "spend_cap_too_low":
|
|
55
|
+
case "insufficient_credits":
|
|
56
|
+
case "workspace_credit_cap_exceeded":
|
|
57
|
+
case "byok_requires_paid_plan":
|
|
58
|
+
return 7;
|
|
59
|
+
case "network_timeout":
|
|
60
|
+
return 8;
|
|
61
|
+
case "provider_error":
|
|
62
|
+
case "upstream_error":
|
|
63
|
+
case "internal_error":
|
|
64
|
+
case "server_error":
|
|
65
|
+
return 5;
|
|
66
|
+
default:
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
if (code === "not_found" || code.endsWith("_not_found"))
|
|
70
|
+
return 4;
|
|
71
|
+
if (code === "timeout" || code.endsWith("_timeout"))
|
|
72
|
+
return 8;
|
|
73
|
+
if (code.startsWith("invalid_")
|
|
74
|
+
|| code.startsWith("missing_")
|
|
75
|
+
|| code === "unknown_fields"
|
|
76
|
+
|| code === "validation_failed") {
|
|
77
|
+
return 2;
|
|
78
|
+
}
|
|
79
|
+
return 1;
|
|
80
|
+
}
|
|
81
|
+
// exitCode 1 is the legacy constructor default at ~170 OxygenError call sites;
|
|
82
|
+
// treat a stored 1 as "unset" and derive the reserved code from the typed
|
|
83
|
+
// error code instead. An explicit non-1 exitCode option always wins.
|
|
84
|
+
export function exitCodeForOxygenError(error) {
|
|
85
|
+
return error.exitCode !== 1 ? error.exitCode : exitCodeForErrorCode(error.code);
|
|
86
|
+
}
|
|
22
87
|
export function success(command, data, version = OXYGEN_VERSION, minimumCliVersion = OXYGEN_MINIMUM_CLI_VERSION) {
|
|
23
88
|
return {
|
|
24
89
|
ok: true,
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export type DeprecationRegistryEntry = {
|
|
2
|
+
/** What is deprecated, precisely enough to grep for. */
|
|
3
|
+
surface: string;
|
|
4
|
+
/** OXYGEN_VERSION at which the surface became deprecated. */
|
|
5
|
+
deprecated_in: string;
|
|
6
|
+
/** Semver deadline, or "floor-bump" (see header). */
|
|
7
|
+
sunset_version: string;
|
|
8
|
+
/** Where the surface lives and what removal entails. */
|
|
9
|
+
note: string;
|
|
10
|
+
};
|
|
11
|
+
export declare const FLOOR_BUMP_SENTINEL = "floor-bump";
|
|
12
|
+
export declare const DEPRECATION_REGISTRY: DeprecationRegistryEntry[];
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Deprecation sunset registry — the build-enforced list of deprecated agent
|
|
2
|
+
// surfaces and when each one must actually be removed.
|
|
3
|
+
//
|
|
4
|
+
// Why this exists: deprecated aliases ("kept for old clients") historically
|
|
5
|
+
// outlive every intention to remove them because nothing ever fails. The
|
|
6
|
+
// registry inverts that: packages/shared/src/index.test.ts asserts that
|
|
7
|
+
// OXYGEN_VERSION has NOT passed any entry's semver `sunset_version`. Once the
|
|
8
|
+
// version crosses a sunset, the build goes red until the surface is deleted —
|
|
9
|
+
// and removal means deleting the surface AND its registry entry in the same
|
|
10
|
+
// PR.
|
|
11
|
+
//
|
|
12
|
+
// sunset_version values:
|
|
13
|
+
// - "1.x.y" semver — hard deadline, gated by the test.
|
|
14
|
+
// - "floor-bump" — sentinel for surfaces that can only be removed when
|
|
15
|
+
// OXYGEN_MINIMUM_CLI_VERSION next rises past the clients that still use
|
|
16
|
+
// them (old CLIs on users' machines send/read these). The test validates
|
|
17
|
+
// the entry's shape but skips the version gate; whoever bumps the CLI
|
|
18
|
+
// floor must sweep the "floor-bump" entries in the same change.
|
|
19
|
+
export const FLOOR_BUMP_SENTINEL = "floor-bump";
|
|
20
|
+
export const DEPRECATION_REGISTRY = [
|
|
21
|
+
{
|
|
22
|
+
surface: "oxygen_templates_* MCP alias tools",
|
|
23
|
+
deprecated_in: "1.80.0",
|
|
24
|
+
sunset_version: "1.290.0",
|
|
25
|
+
note: "Deprecated alias tree of oxygen_prompts_* (packages/mcp-server/src/tools/" +
|
|
26
|
+
"prompt-template-tools.ts, prefix \"oxygen_templates\"). Removal: drop the " +
|
|
27
|
+
"alias prefix from the generated tools and the CLI `templates` alias " +
|
|
28
|
+
"command tree, then update the pinned tool counts.",
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
surface: "oxygen.linkedin-inbox widget alias",
|
|
32
|
+
deprecated_in: "1.226.0",
|
|
33
|
+
sunset_version: "1.290.0",
|
|
34
|
+
note: "Legacy alias of oxygen.unibox kept so MCP clients that cached " +
|
|
35
|
+
"ui://oxygen/linkedin-inbox keep resolving (packages/mcp-server/src/widgets/" +
|
|
36
|
+
"definitions.ts). Removal: delete the alias widget definition and any " +
|
|
37
|
+
"bindings that still point at it.",
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
surface: "CLI deepLink/deep_link response keys",
|
|
41
|
+
deprecated_in: "1.8.2",
|
|
42
|
+
sunset_version: FLOOR_BUMP_SENTINEL,
|
|
43
|
+
note: "web_url has been the canonical deep-link key since at least v1.8.2; " +
|
|
44
|
+
"/api/cli responses still duplicate it as deepLink/deep_link because " +
|
|
45
|
+
"CLIs at or below the current minimum version read the legacy keys. " +
|
|
46
|
+
"Remove the duplicated keys (and the CLI readers) when " +
|
|
47
|
+
"OXYGEN_MINIMUM_CLI_VERSION next rises past those clients.",
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
surface: "CLI --return/--org legacy flags",
|
|
51
|
+
deprecated_in: "1.0.0",
|
|
52
|
+
sunset_version: FLOOR_BUMP_SENTINEL,
|
|
53
|
+
note: "Legacy aliases of --return-mode/--org-id on `oxygen tools run` " +
|
|
54
|
+
"(packages/cli/src/index.ts); aliased since before versioned commits. " +
|
|
55
|
+
"Remove the flags and their request-body mappings when " +
|
|
56
|
+
"OXYGEN_MINIMUM_CLI_VERSION next rises past CLIs that send them.",
|
|
57
|
+
},
|
|
58
|
+
];
|
|
@@ -21,3 +21,28 @@ export type KnowledgePageRenderInput = {
|
|
|
21
21
|
* verbatim with exactly one trailing newline.
|
|
22
22
|
*/
|
|
23
23
|
export declare function renderKnowledgePageMarkdown(page: KnowledgePageRenderInput): string;
|
|
24
|
+
export type ParsedKnowledgePageMarkdown = {
|
|
25
|
+
/** True when the file opened with a `---` frontmatter block (any keys). */
|
|
26
|
+
hasFrontmatter: boolean;
|
|
27
|
+
/** Raw scalar frontmatter values by key (tags stay in their `[a, b]` form here). */
|
|
28
|
+
frontmatter: Record<string, string>;
|
|
29
|
+
id: string | null;
|
|
30
|
+
slug: string | null;
|
|
31
|
+
type: string | null;
|
|
32
|
+
title: string | null;
|
|
33
|
+
status: string | null;
|
|
34
|
+
tags: string[] | null;
|
|
35
|
+
summary: string | null;
|
|
36
|
+
revision: number | null;
|
|
37
|
+
/** Body text below the frontmatter (whole file when none), no trailing newline. */
|
|
38
|
+
body: string;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Parse a mirror page file back into its fields — the inverse of
|
|
42
|
+
* renderKnowledgePageMarkdown, tolerant of human/agent edits (`oxygen knowledge
|
|
43
|
+
* push` runs this on locally edited files). Line-based like the renderer's
|
|
44
|
+
* contract: `key: value` pairs between `---` fences, tags as `[a, b]`. A file
|
|
45
|
+
* without a frontmatter block parses as pure body; a plain-markdown title is
|
|
46
|
+
* derived from the first `# heading` so brand-new local files can push.
|
|
47
|
+
*/
|
|
48
|
+
export declare function parseKnowledgePageMarkdown(contents: string): ParsedKnowledgePageMarkdown;
|
|
@@ -55,3 +55,77 @@ export function renderKnowledgePageMarkdown(page) {
|
|
|
55
55
|
const body = (page.body ?? "").replace(/\r\n/g, "\n").replace(/\n+$/, "");
|
|
56
56
|
return `${lines.join("\n")}\n${body}\n`;
|
|
57
57
|
}
|
|
58
|
+
/**
|
|
59
|
+
* Parse a mirror page file back into its fields — the inverse of
|
|
60
|
+
* renderKnowledgePageMarkdown, tolerant of human/agent edits (`oxygen knowledge
|
|
61
|
+
* push` runs this on locally edited files). Line-based like the renderer's
|
|
62
|
+
* contract: `key: value` pairs between `---` fences, tags as `[a, b]`. A file
|
|
63
|
+
* without a frontmatter block parses as pure body; a plain-markdown title is
|
|
64
|
+
* derived from the first `# heading` so brand-new local files can push.
|
|
65
|
+
*/
|
|
66
|
+
export function parseKnowledgePageMarkdown(contents) {
|
|
67
|
+
const text = contents.replace(/\r\n/g, "\n");
|
|
68
|
+
const lines = text.split("\n");
|
|
69
|
+
const parsed = {
|
|
70
|
+
hasFrontmatter: false,
|
|
71
|
+
frontmatter: {},
|
|
72
|
+
id: null,
|
|
73
|
+
slug: null,
|
|
74
|
+
type: null,
|
|
75
|
+
title: null,
|
|
76
|
+
status: null,
|
|
77
|
+
tags: null,
|
|
78
|
+
summary: null,
|
|
79
|
+
revision: null,
|
|
80
|
+
body: "",
|
|
81
|
+
};
|
|
82
|
+
let bodyStart = 0;
|
|
83
|
+
if (lines[0]?.trim() === "---") {
|
|
84
|
+
const closing = lines.findIndex((line, index) => index > 0 && line.trim() === "---");
|
|
85
|
+
if (closing > 0) {
|
|
86
|
+
parsed.hasFrontmatter = true;
|
|
87
|
+
for (const line of lines.slice(1, closing)) {
|
|
88
|
+
const separator = line.indexOf(":");
|
|
89
|
+
if (separator <= 0)
|
|
90
|
+
continue;
|
|
91
|
+
const key = line.slice(0, separator).trim();
|
|
92
|
+
if (!/^[A-Za-z0-9_-]+$/.test(key))
|
|
93
|
+
continue;
|
|
94
|
+
parsed.frontmatter[key] = line.slice(separator + 1).trim();
|
|
95
|
+
}
|
|
96
|
+
bodyStart = closing + 1;
|
|
97
|
+
// The renderer emits exactly one blank line after the closing fence.
|
|
98
|
+
if (lines[bodyStart] === "")
|
|
99
|
+
bodyStart += 1;
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
parsed.body = lines.slice(bodyStart).join("\n").replace(/\n+$/, "");
|
|
103
|
+
const fm = parsed.frontmatter;
|
|
104
|
+
const str = (key) => {
|
|
105
|
+
const value = fm[key];
|
|
106
|
+
return value !== undefined && value !== "" ? value : null;
|
|
107
|
+
};
|
|
108
|
+
parsed.id = str("id");
|
|
109
|
+
parsed.slug = str("slug");
|
|
110
|
+
parsed.type = str("type");
|
|
111
|
+
parsed.title = str("title");
|
|
112
|
+
parsed.status = str("status");
|
|
113
|
+
parsed.summary = str("summary");
|
|
114
|
+
if (fm.tags !== undefined) {
|
|
115
|
+
const inner = fm.tags.replace(/^\[/, "").replace(/\]$/, "");
|
|
116
|
+
parsed.tags = inner
|
|
117
|
+
.split(",")
|
|
118
|
+
.map((tag) => tag.trim())
|
|
119
|
+
.filter((tag) => tag.length > 0);
|
|
120
|
+
}
|
|
121
|
+
if (fm.revision !== undefined) {
|
|
122
|
+
const revision = Number.parseInt(fm.revision, 10);
|
|
123
|
+
parsed.revision = Number.isFinite(revision) ? revision : null;
|
|
124
|
+
}
|
|
125
|
+
if (!parsed.title) {
|
|
126
|
+
const heading = lines.slice(bodyStart).find((line) => /^#\s+\S/.test(line));
|
|
127
|
+
if (heading)
|
|
128
|
+
parsed.title = heading.replace(/^#\s+/, "").trim() || null;
|
|
129
|
+
}
|
|
130
|
+
return parsed;
|
|
131
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const OXYGEN_VERSION = "1.
|
|
1
|
+
export declare const OXYGEN_VERSION = "1.284.3";
|
|
2
2
|
export declare const OXYGEN_MINIMUM_CLI_VERSION = "1.181.0";
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.284.3";
|
|
2
2
|
// Bump this only when deployed CLI/API contracts require a newer CLI.
|
|
3
3
|
// 1.181.0: paid table action runs and background columns run require
|
|
4
4
|
// approved=true in addition to max_credits; older CLIs cannot send the flag.
|