@oxygen-agent/cli 1.1011.804 → 1.1011.855

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (28) hide show
  1. package/README.md +1 -1
  2. package/dist/auto-update.d.ts +1 -0
  3. package/dist/auto-update.js +1 -1
  4. package/dist/command-manifest.js +12 -3
  5. package/dist/contract-flags.js +12 -4
  6. package/dist/credentials.d.ts +1 -0
  7. package/dist/credentials.js +2 -1
  8. package/dist/customer-operation-commands.js +63 -15
  9. package/dist/help.js +2 -3
  10. package/dist/home-standup.d.ts +15 -0
  11. package/dist/home-standup.js +14 -0
  12. package/dist/index.js +136 -33
  13. package/dist/tool-catalog-detail-command.js +1 -1
  14. package/node_modules/@oxygen/shared/dist/capability-discovery.js +36 -0
  15. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +1 -1
  16. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +2 -2
  17. package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +2 -0
  18. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.d.ts +9 -0
  19. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +19 -0
  20. package/node_modules/@oxygen/shared/dist/contracts/table-operations.d.ts +1 -0
  21. package/node_modules/@oxygen/shared/dist/contracts/table-operations.js +5 -4
  22. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +4 -0
  23. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +10 -1
  24. package/node_modules/@oxygen/shared/dist/table-capacity.d.ts +8 -0
  25. package/node_modules/@oxygen/shared/dist/table-capacity.js +8 -0
  26. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  27. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  28. package/package.json +1 -1
package/README.md CHANGED
@@ -34,4 +34,4 @@ oxygen update
34
34
 
35
35
  For product documentation, visit https://oxygen-agent.com/docs. For support, visit https://oxygen-agent.com.
36
36
 
37
- Version: 1.1011.804
37
+ Version: 1.1011.855
@@ -64,6 +64,7 @@ export declare function autoUpdateStatePath(env?: NodeJS.ProcessEnv): string;
64
64
  export declare function readAutoUpdateState(env?: NodeJS.ProcessEnv): AutoUpdateState;
65
65
  /** Best-effort: an unwritable config directory must never fail a command. */
66
66
  export declare function writeAutoUpdateState(state: AutoUpdateState, env?: NodeJS.ProcessEnv): boolean;
67
+ export declare function isTruthyEnv(value: string | undefined): boolean;
67
68
  export declare function autoUpdateEligibility(runtime?: AutoUpdateRuntime & {
68
69
  endpoint?: CliEndpoint;
69
70
  state?: AutoUpdateState;
@@ -91,7 +91,7 @@ export function writeAutoUpdateState(state, env = process.env) {
91
91
  return false;
92
92
  }
93
93
  }
94
- function isTruthyEnv(value) {
94
+ export function isTruthyEnv(value) {
95
95
  const normalized = value?.trim().toLowerCase();
96
96
  return Boolean(normalized && normalized !== "0" && normalized !== "false");
97
97
  }
@@ -101,6 +101,8 @@ const MUTATING_VERBS = new Set([
101
101
  // status persists state and can pause the provider; reconnect replaces its
102
102
  // connection and can restart provider history. Other status commands are reads.
103
103
  const MUTATING_COMMANDS = new Set([
104
+ // A default preview is free; approval restores provider seats and billing.
105
+ "managed-inboxes recover",
104
106
  "mailboxes warmup status",
105
107
  "mailboxes warmup reconnect",
106
108
  // A noun leaf with its own action (not a group), so the default would
@@ -112,9 +114,15 @@ const MUTATING_COMMANDS = new Set([
112
114
  "viewers enrichment",
113
115
  // A noun leaf that switches a bought domain's yearly paid renewal on or off.
114
116
  "domains renewal",
115
- // The standup reads AND moves the user's Home "last seen" marker, so the next
116
- // standup, on the web too, starts from this call. An agent assessing a
117
- // workspace must not mistake it for a pure read (2026-10-04 blind acceptance).
117
+ // The plain standup view in an interactive terminal moves the user's Home
118
+ // "last seen" marker, as a web Home visit does; `--json`, or output that is not
119
+ // a terminal (an agent's shell, a pipe), is a pure read that leaves it alone
120
+ // (founder decision, 2026-10-05; home-standup.ts). `mutates` is one bit per
121
+ // command and cannot say "only in a terminal without --json", so it keeps the
122
+ // capability answer every other mixed command gives — some invocation writes —
123
+ // while the `--json` flag's own description names the read. Reporting false
124
+ // would advertise the person's terminal view, the one that moves the marker,
125
+ // as a read.
118
126
  "home",
119
127
  ]);
120
128
  // `--approved` usually identifies a paid or externally mutating execution
@@ -182,6 +190,7 @@ const READ_ONLY_COMMANDS = new Set([
182
190
  // invocation still previews, so machine-readable discovery must not imply that
183
191
  // calling the command without its confirmation flag writes anything.
184
192
  const PREVIEW_BY_DEFAULT_COMMANDS = new Set([
193
+ "managed-inboxes recover",
185
194
  // /api/cli/find/run treats an omitted mode as dry_run for every Find capability.
186
195
  "find email",
187
196
  "find personal-email",
@@ -17,6 +17,7 @@ import { Option } from "commander";
17
17
  import { OxygenError } from "@oxygen/shared/cli-result";
18
18
  import { unwrapNullable } from "@oxygen/shared/contracts/schema";
19
19
  const MAX_SPLAT_FIELDS = 6;
20
+ const BOOLEAN_HELP_COMMANDS = new WeakSet();
20
21
  function kebab(name) {
21
22
  return name.replace(/_/g, "-");
22
23
  }
@@ -74,7 +75,12 @@ function propertyAt(contract, path) {
74
75
  }
75
76
  /** Registers the contract's flags on `command`, in contract order. */
76
77
  export function addContractFlags(command, contract, overlay = {}) {
77
- for (const spec of contractFlagSpecs(contract.properties, overlay)) {
78
+ const specs = contractFlagSpecs(contract.properties, overlay);
79
+ if (specs.some((spec) => spec.kind === "boolean" || spec.kind === "pair") && !BOOLEAN_HELP_COMMANDS.has(command)) {
80
+ command.addHelpText("after", "\nBoolean flags: positive sends true; negative sends false; omitted fields are not sent.");
81
+ BOOLEAN_HELP_COMMANDS.add(command);
82
+ }
83
+ for (const spec of specs) {
78
84
  const property = "path" in spec ? propertyAt(contract, spec.path) : undefined;
79
85
  if (spec.kind === "pair") {
80
86
  command.addOption(new Option(`--${spec.on}`, spec.entry.pair?.onHelp));
@@ -86,7 +92,7 @@ export function addContractFlags(command, contract, overlay = {}) {
86
92
  continue;
87
93
  }
88
94
  if (spec.kind === "boolean") {
89
- command.addOption(new Option(`--${spec.flag}`, helpFor(spec, property)));
95
+ command.addOption(new Option(`--${spec.flag}`, helpFor(spec, property) || `Set ${spec.path.join(".")}: true.`));
90
96
  if (!spec.entry.noNegate) {
91
97
  command.addOption(new Option(`--no-${spec.flag}`, spec.entry.negateHelp ?? `Sends ${spec.path.join(".")}: false.`));
92
98
  }
@@ -100,7 +106,7 @@ export function addContractFlags(command, contract, overlay = {}) {
100
106
  return command;
101
107
  }
102
108
  function invalid(flag, value, message, code = "invalid_number") {
103
- return new OxygenError(code, message, { details: { flag: `--${flag}`, value }, exitCode: 2 });
109
+ return new OxygenError(code, message, { details: { flag: `--${flag}`, value, next_step: `Correct --${flag}. ${message}` }, exitCode: 2 });
104
110
  }
105
111
  function parseScalar(property, raw, flag) {
106
112
  if (!("type" in property))
@@ -211,7 +217,9 @@ export function readContractFlags(options, contract, overlay = {}) {
211
217
  }
212
218
  else if (options[attribute(spec.flag)] === true) {
213
219
  if (given.has(spec.target)) {
214
- throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, { exitCode: 2 });
220
+ throw new OxygenError("invalid_request", `Pass either --${spec.target} or --${spec.flag}, not both.`, {
221
+ details: { next_step: `Remove --${spec.flag} to keep the value, or remove --${spec.target} to clear it.` }, exitCode: 2,
222
+ });
215
223
  }
216
224
  setPath(body, spec.path, null);
217
225
  }
@@ -79,4 +79,5 @@ export declare function updateActiveOrganizationForProfile(profile: string, orga
79
79
  export declare function normalizeApiUrl(value: string): string;
80
80
  /** The CLI's per-user config directory; `OXYGEN_CONFIG_DIR` overrides it. */
81
81
  export declare function resolveConfigDir(env?: NodeJS.ProcessEnv): string;
82
+ export declare function readEnvToken(env?: NodeJS.ProcessEnv): string | null;
82
83
  export {};
@@ -769,7 +769,8 @@ function readProfiles(document) {
769
769
  }
770
770
  return profiles;
771
771
  }
772
- function readEnvToken(env) {
772
+ // When this is set, every request runs on it and no stored profile is read.
773
+ export function readEnvToken(env = process.env) {
773
774
  const token = env.OXYGEN_API_KEY ?? env.OXYGEN_TOKEN;
774
775
  return token?.trim() || null;
775
776
  }
@@ -5,16 +5,57 @@ import { CLI_OPERATIONS, operationFormData, operationInputContract, operationReq
5
5
  import { unwrapNullable, validateContract } from "@oxygen/shared/contracts/schema";
6
6
  import { addContractFlags, readContractFlags } from "./contract-flags.js";
7
7
  import { requestOxygen } from "./http-client.js";
8
+ // Only newly registered groups use these purposes; manual owners retain their
9
+ // descriptions, aliases and children.
10
+ const GROUP_PURPOSES = {
11
+ "copilot policy": "Set an attended Copilot session's approval and screen-follow policies.",
12
+ "context canonical": "Read the workspace's pinned canonical context asset.",
13
+ "directory logo": "Upload the public agency listing's logo.",
14
+ "managed-inboxes avatars": "Prepare and confirm managed mailbox profile pictures.",
15
+ "settings beta-features": "Read or change the workspace's beta navigation opt-in.",
16
+ spintax: "Generate message variations with the declared credit cost.",
17
+ "table-actions": "Resolve an uncertain Table action from a verified outcome.",
18
+ "whatsapp drafts": "Read and edit saved WhatsApp drafts before sending.",
19
+ sessions: "Read or revoke your personal CLI and OAuth sessions.",
20
+ "linkedin followers": "Inspect durable follower capture progress into a Table.",
21
+ "linkedin inbox": "Inspect LinkedIn inbox backfill progress.",
22
+ "linkedin viewers": "Read named LinkedIn profile viewers within provider privacy limits.",
23
+ pricing: "Read customer credit prices and their provenance.",
24
+ "sequences leads": "Read enrolled Sequence leads, steps and action outcomes.",
25
+ unibox: "Read inbox accounts, filters and tags for message triage.",
26
+ "tables rows": "Read and edit scoped Table rows and CRM relationships.",
27
+ "tables runs": "Preview, run and recover Table actions within approved scope.",
28
+ "tables ingestion": "Stop or retry hosted Table ingestion.",
29
+ "billing ownership": "Read or change a workspace's shared billing owner.",
30
+ "notetaker calendar": "Connect calendars, sync meetings and approve meeting recording.",
31
+ "notetaker notes": "Edit saved meeting notes.",
32
+ onboarding: "Save workspace discovery choices and continue onboarding.",
33
+ };
34
+ const OPERATION_FLAGS = {
35
+ "tables favorite": { favorite: { negateHelp: "Unstar the selected Table or folder (favorite: false)." } },
36
+ "tables favorites": { details: { negateHelp: "Return stored star keys without names or links (details: false)." } },
37
+ };
38
+ function usageRecovery(error, path) {
39
+ if (!(error instanceof OxygenError) || error.exitCode !== 2)
40
+ return error;
41
+ const details = error.details && typeof error.details === "object" && !Array.isArray(error.details)
42
+ ? error.details : {};
43
+ const help = `Run \`oxygen ${path.join(" ")} --help\` for its options.`;
44
+ return new OxygenError(error.code, error.message, {
45
+ exitCode: error.exitCode,
46
+ details: { ...details, next_step: typeof details.next_step === "string" ? `${details.next_step} ${help}` : help },
47
+ });
48
+ }
8
49
  /** Register each domain's selected operation as a native command with exact typed flags. */
9
50
  export function registerCustomerOperationCommands(program, handle) {
10
51
  for (const spec of CLI_OPERATIONS) {
11
52
  let parent = program;
12
53
  const words = [...spec.cli.path];
13
54
  const leaf = words.pop();
14
- for (const word of words) {
55
+ for (const [index, word] of words.entries()) {
15
56
  let child = parent.commands.find((command) => command.name() === word);
16
57
  if (!child)
17
- child = parent.command(word).description(`${word} workspace operations.`);
58
+ child = parent.command(word).description(GROUP_PURPOSES[words.slice(0, index + 1).join(" ")] ?? spec.cli.summary);
18
59
  parent = child;
19
60
  }
20
61
  // Never hide an existing owner behind a second Commander command.
@@ -22,7 +63,8 @@ export function registerCustomerOperationCommands(program, handle) {
22
63
  throw new Error(`Duplicate native operation command ${spec.cli.path.join(" ")}`);
23
64
  }
24
65
  const contract = operationInputContract(spec);
25
- const command = addContractFlags(parent.command(leaf).description(spec.cli.summary), contract)
66
+ const overlay = OPERATION_FLAGS[spec.cli.path.join(" ")] ?? {};
67
+ const command = addContractFlags(parent.command(leaf).description(spec.cli.summary), contract, overlay)
26
68
  .option("--json", "Print a JSON envelope.");
27
69
  if (spec.binary)
28
70
  command.requiredOption("--output <path>", "Write the exact downloaded bytes to this local file.");
@@ -48,18 +90,24 @@ export function registerCustomerOperationCommands(program, handle) {
48
90
  }
49
91
  }
50
92
  command.action((options, selected) => handle(spec.cli.path.join(" "), { ...options, json: selected.optsWithGlobals().json === true }, async () => {
51
- const input = readContractFlags(options, contract);
52
- if (spec.multipart && options.file !== undefined) {
53
- if (input[spec.multipart.contentField] !== undefined)
54
- throw new OxygenError("invalid_request", "Choose --file or the base64 content flag, not both.", { exitCode: 2 });
55
- const filePath = String(options.file);
56
- input[spec.multipart.contentField] = (await readFile(filePath)).toString("base64");
57
- input[spec.multipart.nameField] ??= basename(filePath);
58
- input[spec.multipart.typeField] ??= uploadContentType(filePath);
93
+ let input;
94
+ try {
95
+ input = readContractFlags(options, contract, overlay);
96
+ if (spec.multipart && options.file !== undefined) {
97
+ if (input[spec.multipart.contentField] !== undefined)
98
+ throw new OxygenError("invalid_request", "Choose --file or the base64 content flag, not both.", { exitCode: 2 });
99
+ const filePath = String(options.file);
100
+ input[spec.multipart.contentField] = (await readFile(filePath)).toString("base64");
101
+ input[spec.multipart.nameField] ??= basename(filePath);
102
+ input[spec.multipart.typeField] ??= uploadContentType(filePath);
103
+ }
104
+ const validation = validateContract(contract, input, { unknown: "reject", overMax: "reject" });
105
+ if (!validation.ok)
106
+ throw new OxygenError("invalid_request", validation.issues.map((issue) => issue.message).join(" "), { exitCode: 2 });
107
+ }
108
+ catch (error) {
109
+ throw usageRecovery(error, spec.cli.path);
59
110
  }
60
- const validation = validateContract(contract, input, { unknown: "reject", overMax: "reject" });
61
- if (!validation.ok)
62
- throw new OxygenError("invalid_request", validation.issues.map((issue) => issue.message).join(" "), { exitCode: 2 });
63
111
  const request = operationRequest(spec, input);
64
112
  if (spec.binary) {
65
113
  if (spec.binary.stream)
@@ -83,7 +131,7 @@ export function registerCustomerOperationCommands(program, handle) {
83
131
  formData = operationFormData(spec, request.body ?? {});
84
132
  }
85
133
  catch (error) {
86
- throw new OxygenError("invalid_request", error instanceof Error ? error.message : "Invalid upload.", { exitCode: 2 });
134
+ throw usageRecovery(new OxygenError("invalid_request", error instanceof Error ? error.message : "Invalid upload.", { exitCode: 2 }), spec.cli.path);
87
135
  }
88
136
  return requestOxygen(request.path, { method: request.method, formData });
89
137
  }
package/dist/help.js CHANGED
@@ -137,7 +137,7 @@ export function applyOxygenHelp(program, binaryName) {
137
137
  const exitCodeLine = Object.entries(CLI_EXIT_CODE_TABLE)
138
138
  .map(([code, meaning]) => `${code} ${meaning.split(" (")[0]}`)
139
139
  .join(" · ");
140
- program.addHelpText("afterAll", [
140
+ program.addHelpText("before", [
141
141
  "",
142
142
  "Agent quickstart:",
143
143
  ` 1. ${binaryName} login connect this terminal (or set OXYGEN_API_KEY)`,
@@ -166,9 +166,8 @@ export function applyOxygenHelp(program, binaryName) {
166
166
  " Credit-spending commands require --approved --max-credits <n>; zero-credit",
167
167
  " external writes require --approved plus the command's preview-bound plan token.",
168
168
  " State-changing responses include a web_url deep-link to inspect the result.",
169
- "",
170
- `Exit codes: ${exitCodeLine}`,
171
169
  ].join("\n"));
170
+ program.addHelpText("afterAll", `\nExit codes: ${exitCodeLine}`);
172
171
  }
173
172
  /**
174
173
  * Turn commander's own edit-distance suggester on for EVERY command.
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Whether an `oxygen home` read is the person looking, and so marks Home as seen.
3
+ *
4
+ * Founder decision, 2026-10-05: "`oxygen home --json` and MCP reads leave the marker
5
+ * alone; your web visit and the plain terminal view still reset it." A terminal view
6
+ * is a person reading a terminal. `--json` is a machine read, and so is the plain view
7
+ * when stdout is not a terminal: in all four 2026-10-04 blind consultant CLI runs the
8
+ * agent ran `oxygen home 2>&1 | head -150`, a pipe its shell captured, and that read
9
+ * moved the founder's "since you last looked" window. Node reports a pipe or a file as
10
+ * `isTTY: undefined`, so only a literal `true` counts as a terminal.
11
+ */
12
+ export declare function homeStandupMarksSeen(input: {
13
+ json?: boolean | undefined;
14
+ isTTY?: boolean | undefined;
15
+ }): boolean;
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Whether an `oxygen home` read is the person looking, and so marks Home as seen.
3
+ *
4
+ * Founder decision, 2026-10-05: "`oxygen home --json` and MCP reads leave the marker
5
+ * alone; your web visit and the plain terminal view still reset it." A terminal view
6
+ * is a person reading a terminal. `--json` is a machine read, and so is the plain view
7
+ * when stdout is not a terminal: in all four 2026-10-04 blind consultant CLI runs the
8
+ * agent ran `oxygen home 2>&1 | head -150`, a pipe its shell captured, and that read
9
+ * moved the founder's "since you last looked" window. Node reports a pipe or a file as
10
+ * `isTTY: undefined`, so only a literal `true` counts as a terminal.
11
+ */
12
+ export function homeStandupMarksSeen(input) {
13
+ return input.json !== true && input.isTTY === true;
14
+ }
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import { renderPrimaryProviderBoard } from "./admin-primary-providers-render.js";
2
- import { markStdinConsumed, maybeScheduleBackgroundUpdate, readAutoUpdateStatus, runBackgroundUpdate, } from "./auto-update.js";
2
+ import { isTruthyEnv, markStdinConsumed, maybeScheduleBackgroundUpdate, readAutoUpdateStatus, runBackgroundUpdate, } from "./auto-update.js";
3
3
  import { createBrowserLoginSession, createRelayLoginSession, openBrowser } from "./browser-login.js";
4
4
  import { assertModeFlagsExclusive, parseKeyValuePairs, parseJsonObject, readJsonObjectOption, readNonNegativeInt, readPositiveInt, readRecordString, resolveLiveDryRunMode, } from "./cli-values.js";
5
5
  import { readColumnDecisionFlags } from "./column-decision-options.js";
@@ -7,7 +7,7 @@ import { formatAiPromptPreviewNotice, formatColumnReferenceNotices, formatEnrich
7
7
  import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
8
8
  import { registerConnectorsCommands } from "./connectors-commands.js";
9
9
  import { addContractFlags, readContractFlags } from "./contract-flags.js";
10
- import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
10
+ import { clearCredentials, defaultApiUrl, listCredentialProfiles, loadCredentials, normalizeApiUrl, pickProfileNameForIdentity, pickProfileNameForUserSession, readEnvToken, resolveActiveProfile, saveCredentials, switchCredentialProfile, updateActiveOrganizationForProfile, } from "./credentials.js";
11
11
  import { registerCrmFileReadCommands } from "./crm-file-read-commands.js";
12
12
  import { registerCrmSupportFlags, readCrmNoteEditFields, readCrmPhotoFields, readCrmFileLabel, buildCrmObjectsPath, buildCrmConnectionsPath } from "./crm-support-flags.js";
13
13
  import { registerCustomerOperationCommands } from "./customer-operation-commands.js";
@@ -15,6 +15,7 @@ import { DATABASE_QUERY_FLAG_OVERLAY } from "./database-query-flags.js";
15
15
  import { addFeedReadFlags, feedReadWindowParams } from "./feed-read-flags.js";
16
16
  import { registerFunctionsCommands } from "./functions-commands.js";
17
17
  import { applyOxygenHelp, argvRequestsJson, commanderUsageFailure, commandNameForArgv, enableCommandSuggestions, unknownCommandHint, unknownOptionHint, withoutHelpForUnknownCommand, validateRequiredOptionValues, } from "./help.js";
18
+ import { homeStandupMarksSeen } from "./home-standup.js";
18
19
  import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
19
20
  import { formatInboxNeedsReplyNotice } from "./inbox-needs-reply-notice.js";
20
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";
@@ -3786,10 +3787,10 @@ export function createProgram() {
3786
3787
  });
3787
3788
  program
3788
3789
  .command("home")
3789
- .description("Your workspace standup: what happened since you last looked, what needs you, and a year of workspace activity. 0 credits. It marks Home as seen, so the next standup (here or on the web) counts from now. The same neutral composition the web Home renders, so a terminal-first operator is not sent to the browser for a status check.")
3790
- .option("--json", "Print a JSON envelope.")
3790
+ .description("Your workspace standup: what happened since you last looked, what needs you, and a year of workspace activity. 0 credits. With --json, or when the output is not a terminal (an agent's shell or a pipe), it is a pure read: the same window, and your Home \"last seen\" marker stays where you left it. Only the plain view in a terminal counts as you looking: like opening Home on the web, it marks Home as seen (at most every four hours), so your next standup counts from then.")
3791
+ .option("--json", "Print a JSON envelope. A pure read: leaves your Home \"last seen\" marker alone.")
3791
3792
  .action(async (options) => {
3792
- await handleAsyncAction("home standup", options, readHomeStandup);
3793
+ await handleAsyncAction("home standup", options, () => readHomeStandup(options));
3793
3794
  });
3794
3795
  // `oxygen workspace map` sits beside `oxygen home` on purpose: home answers
3795
3796
  // "what needs me", the map answers "what is in here". Both are Control-surface
@@ -6367,10 +6368,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6367
6368
  .addCommand(new Command("import")
6368
6369
  .description("Import provider records into an Oxygen CRM object (canonical CRM truth) as a durable workflow run. Defaults to dry-run. This pulls the WHOLE object and has no list/segment filter; to import one HubSpot saved list or one Pipedrive saved filter into a new table you can filter and enrich, use `oxygen tables hubspot import` or `oxygen tables pipedrive import` instead.")
6369
6370
  .argument("<provider>", "Connected CRM to import from: hubspot, attio, or pipedrive.")
6370
- .requiredOption("--object <object>", "Provider object: hubspot contacts|companies, attio people|companies, or pipedrive persons|organizations. Pipedrive deals have no natural identity key and are not importable yet.")
6371
+ .requiredOption("--object <object>", "Provider object: hubspot contacts|companies, attio people|companies, or pipedrive persons|organizations|deals. Import Pipedrive persons and organizations before deals so each deal links to them.")
6371
6372
  .option("--into <object>", "Oxygen CRM object slug to import into. Defaults to the natural mapping.")
6372
6373
  .option("--limit <n>", "Max records to pull this run.")
6373
- .option("--max-credits <n>", "Volume + external-quota cap (BYOK reads cost 0 Oxygen credits but use your CRM API quota).")
6374
+ .option("--max-credits <n>", "OXYGEN spend ceiling for the run. Own-API-key calls cost 0.05 credits each; OAuth follows its tool quote, and Workflow steps are billed separately. Use --limit for rows; CRM API quota also applies.")
6374
6375
  .option("--approved", "Confirm a live import after inspecting a dry run.")
6375
6376
  .option("--dry-run", "Preview the import without pulling. Default.")
6376
6377
  .option("--live", "Run the live import. Requires --approved and --max-credits.")
@@ -6388,7 +6389,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6388
6389
  .option("--direction <direction>", "Sync direction: inbound, outbound, or bidirectional. Defaults to bidirectional.")
6389
6390
  .option("--cron <cron>", "Cron expression for the scheduled reconciliation cadence, e.g. '*/15 * * * *'.")
6390
6391
  .option("--timezone <tz>", "IANA timezone the cron schedule runs in, e.g. America/New_York. Defaults to UTC.")
6391
- .option("--max-rows <n>", "Per-cycle row cap. The real volume brake on provider writes (BYOK writes cost 0 Oxygen credits).")
6392
+ .option("--max-rows <n>", "Per-cycle row cap, separate from the --max-credits OXYGEN spend ceiling. OAuth provider calls follow their tool quote; own-API-key calls and live Workflow steps have separate charges.")
6392
6393
  .option("--max-credits <n>", "Managed-credit + external-quota cap per cycle.")
6393
6394
  .option("--field-mappings-json <json>", "HubSpot custom-property mappings as a JSON array of {oxygen_field, provider_field, direction}. Pass [] to clear; canonical identity/name mappings stay fixed.")
6394
6395
  .option("--approved", "Confirm a live configuration after inspecting a dry run.")
@@ -10491,7 +10492,7 @@ Examples:
10491
10492
  }));
10492
10493
  program
10493
10494
  .command("table-runs")
10494
- .description("Durable background table action run commands. `list --table <table> --status all` is a table's full run history, including Function runs launched from it; without --status, list shows only active runs.")
10495
+ .description("Durable background table action run commands. `list --table <table> --status all` is a table's full action-run history, including Function runs launched from it; without --status, list shows only active runs. Imports and other writes: `runs list --table <table>`.")
10495
10496
  .addCommand(new Command("create")
10496
10497
  .description("Create a durable background run for table tool-column actions. Large runs are accepted immediately and their rows are prepared in the background (execution_status 'planning'); follow with `table-runs wait <run_id>`.")
10497
10498
  .argument("<table>", "Table id or slug.")
@@ -10533,9 +10534,9 @@ Examples:
10533
10534
  }));
10534
10535
  }))
10535
10536
  .addCommand(new Command("list")
10536
- .description("List durable table action runs for one table.")
10537
+ .description("List durable table action runs for one table. For every run of the table, imports included, use `runs list --table <table>`.")
10537
10538
  .requiredOption("--table <table>", "Table id or slug.")
10538
- .option("--status <status>", "Filter by all, active, queued, running, paused, completed, completed_with_errors, failed, canceling, or canceled. Defaults to active; pass all for the full history in one call, including Function runs launched from this table.")
10539
+ .option("--status <status>", "Filter by all, active, queued, running, paused, completed, completed_with_errors, failed, canceling, or canceled. Defaults to active; pass all for the full action-run history in one call, including Function runs launched from this table.")
10539
10540
  .option("--limit <n>", "Maximum runs to return. Defaults to 20.")
10540
10541
  .option("--format <format>", "Format run history as json, jsonl, csv, or table (same serializer as `tables export`). Omit to keep the default envelope output.")
10541
10542
  .option("--output <path>", "Write the formatted run history to a file instead of embedding it in the response.")
@@ -10645,7 +10646,7 @@ Examples:
10645
10646
  }));
10646
10647
  program
10647
10648
  .command("table-ingestions")
10648
- .description("Durable background table ingestion commands.")
10649
+ .description("Durable background table ingestion commands. To find a table's ingestion runs: `runs list --table <table>`.")
10649
10650
  .addCommand(new Command("create-tool-page")
10650
10651
  .description("Create a paged tool-source ingestion run for a workspace table.")
10651
10652
  .argument("<table>", "Table id or slug.")
@@ -12666,15 +12667,23 @@ Examples:
12666
12667
  }));
12667
12668
  program
12668
12669
  .command("runs")
12669
- .description("Tenant run provenance commands.")
12670
+ .description("Workspace run ledger and provenance. `runs list --table <table>` is one table's full run history: imports, row writes and column runs.")
12670
12671
  .addCommand(new Command("list")
12671
- .description("List recent tenant runs.")
12672
+ .description("List recent tenant runs, newest first.")
12673
+ .option("--table <table>", "Only runs of this table (id, slug or table URL), e.g. to check whether a column run started.")
12672
12674
  .option("--limit <n>", "Maximum runs to return. Defaults to 50.")
12673
12675
  .option("--json", "Print a JSON envelope.")
12674
12676
  .action(async (options) => {
12675
12677
  await handleAsyncAction("runs list", options, () => {
12676
12678
  const limit = readPositiveInt(options.limit);
12677
- return requestOxygen(`/api/cli/runs${limit ? `?limit=${limit}` : ""}`);
12679
+ const params = new URLSearchParams();
12680
+ const table = readOption(options.table);
12681
+ if (table)
12682
+ params.set("table", table);
12683
+ if (limit)
12684
+ params.set("limit", String(limit));
12685
+ const query = params.toString();
12686
+ return requestOxygen(`/api/cli/runs${query ? `?${query}` : ""}`);
12678
12687
  });
12679
12688
  }))
12680
12689
  .addCommand(new Command("get")
@@ -12917,16 +12926,16 @@ Examples:
12917
12926
  }));
12918
12927
  program
12919
12928
  .command("tools")
12920
- .description("The priced catalog: every provider operation and every OXYGEN column (enrichment bundles, waterfalls, templates, Functions) with its credit cost per row. Start with `tools search --for-table <table>` to see what a table's own columns can be enriched with and what each costs, before adding anything.")
12929
+ .description("Find provider operations and OXYGEN columns. For one provider operation's effective price on this workspace's credential, use `tools get <tool_id> --view pricing --json`; for a request-specific quote at 0 credits and without a provider call, use `tools run <tool_id> --input-json '{}' --mode dry-run --json`. Use `tools search --for-table <table>` to price a table's enrichment columns per row.")
12921
12930
  .addCommand(addToolCatalogSearchWindowFlag(new Command("search")
12922
- .description("Search the priced catalog (0 credits): provider operations in `tools`, and OXYGEN's own columns in `native_columns` (enrichment bundles, waterfalls, templates, Functions), each with `price_label`, `estimated_credits_per_row`, `pricing_kind` and the exact `cli_add` command. With --for-table <table> it reads that table's columns and returns `suggestions`: the enrichments those columns already support, priced per row. Hydrate one provider operation with tools get. A response listing partial_sources means an optional catalog source timed out and totals may be understated — rerun for the complete catalog.")
12931
+ .description("Search provider operations and OXYGEN columns for 0 credits; the default list is bounded. Use `tools get <tool_id> --view pricing --json` for one workspace-effective fee, or `tools run <tool_id> --input-json '{}' --mode dry-run --json` for a 0-credit quote without a provider call. With --for-table, suggestions prices enrichments per row. If partial_sources appears, totals may be understated; rerun.")
12923
12932
  .argument("[query]", "Search text, e.g. `web` for the Web Research Agent column and its modes (each priced in `modes`), or `email`.")
12924
12933
  .option("--verbosity <verbosity>", "minimal, summary, or full. Defaults to minimal; hydrate one result with tools get.")
12925
12934
  .option("--terse", "Alias for --verbosity minimal.")
12926
12935
  .option("--all", "Return the complete matching catalog — with no query that is every provider operation (7,000+ rows). Explicit because the default is bounded to 10; with --for-table the table-scoped answer is `suggestions`, which --all does not change.")
12927
12936
  .option("--only-runnable", "Only return tools runnable by the active organization.")
12928
12937
  .option("--workflow-eligible", "Only return tools a hosted workflow step may call, each annotated with the canonical `workflow_effect` its manifest step must declare. This is a narrower set than the table-column catalog — use it when authoring a workflow manifest so lint cannot reject a tool at apply time.")
12929
- .option("--no-access-check", "Skip per-tool availability checks for a fast complete-catalog listing. Tools are returned without availability info; pair with --terse for discovery sweeps.")
12938
+ .option("--no-access-check", "Skip workspace availability checks for a fast catalog listing. Prices then show managed list estimates, not this workspace's effective credential fee; use tools get <tool_id> --view pricing for that. Pair with --terse for discovery sweeps.")
12930
12939
  .option("--category <category>", "Filter by Oxygen tool category, such as company_search, people_search, research, or local.")
12931
12940
  .option("--capability <tag>", "Filter by capability tag, such as mobile_phone.")
12932
12941
  .option("--provider <provider>", "Filter to one exact provider id, such as blitzapi.")
@@ -13044,10 +13053,10 @@ Examples:
13044
13053
  });
13045
13054
  })))
13046
13055
  .addCommand(new Command("run")
13047
- .description("Run an executable Oxygen tool through the HTTPS API.")
13056
+ .description("Preview or run one tool. `--mode dry-run` quotes the exact inputs at 0 credits without a provider call or data write; `--mode live` executes after any required approval and credit ceiling.")
13048
13057
  .argument("<tool_id>", "Tool id.")
13049
13058
  .requiredOption("--input-json <json>", "Tool input as a JSON object.")
13050
- .option("--mode <mode>", "Execution mode: live or dry-run. Mutating tools default to dry-run.")
13059
+ .option("--mode <mode>", "Execution mode: live or dry-run. Dry-run costs 0 credits and makes no provider call or data write; mutating tools default to dry-run.")
13051
13060
  .option("--credential-mode <mode>", "Credential mode: managed, user_api_key, or user_oauth.")
13052
13061
  .option("--org <organization_id>", "Optional organization id. Must match the stored CLI token.")
13053
13062
  .option("--org-id <organization_id>", "Optional organization id. Must match the stored CLI token.")
@@ -14874,7 +14883,7 @@ Examples:
14874
14883
  .option("--channels <list>", "Narrow the merged stream: comma-separated channel groups to include (email,linkedin,whatsapp). Implies --channel all.")
14875
14884
  .option("--account <id>", "LinkedIn only: filter to one sender account (sender id, connection id, or Unipile account id).")
14876
14885
  .option("--unread", "Only show conversations with unread messages.")
14877
- .option("--unanswered", "Only conversations you have not replied to — the last message in the thread is inbound. Not the same as needing a reply: it keeps thank-yous and automated mail. Cross-channel. Off by default: an unfiltered list still shows answered threads. (The web Unibox turns this on by default for its Primary tab.) It includes inbound mail no campaign sent — unsolicited or spam — so for replies to your outreach add --sequence-id; the ids and per-campaign counts are in sidebar_counts.byCampaign of the same --json response.")
14886
+ .option("--unanswered", "Only conversations you have not replied to — the last message in the thread is inbound. Not the same as needing a reply: it keeps thank-yous and automated mail. Cross-channel. Off by default in the CLI and web Unibox: an unfiltered list still shows answered threads. It includes inbound mail no campaign sent — unsolicited or spam — so for replies to your outreach add --sequence-id; the ids and per-campaign counts are in sidebar_counts.byCampaign of the same --json response.")
14878
14887
  // Hidden while the AI inbox triage is dev-only (ADR 0027): production
14879
14888
  // help must not offer a flag its API refuses. Unhide with the flag's
14880
14889
  // production rollout.
@@ -14884,13 +14893,14 @@ Examples:
14884
14893
  .option("--segment <segment>", "Top tab: primary (everything but the negative status tier), all, or an email-only folder (others, sent, warmup, dmarc).")
14885
14894
  .option("--status <keys>", "Comma-separated reply-status keys, set by AI reply triage. System keys: interested, neutral, out_of_office, wrong_person, not_now, not_interested, lost, bounced; your workspace's full list, custom labels included, is `inbox labels list`. Cross-channel — filters email + LinkedIn + WhatsApp by the shared taxonomy.")
14886
14895
  .option("--tag <tags>", "Comma-separated campaign tags — matches email conversations whose campaign (sequence) carries any of these tags. DMs have no campaign link, so a tag filter shows email only.")
14887
- .option("--since <iso>", "Only conversations whose last message is on/after this ISO date/timestamp. Cross-channel.")
14888
- .option("--until <iso>", "Only conversations whose last message is on/before this ISO date/timestamp. Cross-channel.")
14896
+ .option("--since <iso>", "Only conversations whose last message is on/after this ISO date/timestamp. Excludes conversations without a last-message date. Cross-channel.")
14897
+ .option("--until <iso>", "Only conversations whose last message is on/before this ISO date/timestamp. Excludes conversations without a last-message date. Cross-channel.")
14889
14898
  .option("--sequence-id <ids>", "Email only: comma-separated campaign (sequence) UUIDs (not slugs — `sequences list` shows each campaign's id). Returns only conversations linked to that campaign; `sidebar_counts.byCampaign` is its size.")
14890
14899
  .option("--provider <providers>", "Email only: comma-separated providers (google,microsoft).")
14891
14900
  .option("--domain <domains>", "Email only: comma-separated counterpart domains to include.")
14892
14901
  .option("--exclude-domain <domains>", "Email only: comma-separated counterpart domains to exclude.")
14893
14902
  .option("--mailbox-id <ids>", "Email only: comma-separated mailbox ids.")
14903
+ .option("--sender-profile <ids>", "Comma-separated sender profile ids (people, from `senders profiles list`): only conversations on their own inboxes and LinkedIn/WhatsApp accounts. Merged stream only; narrow channels with --channels.")
14894
14904
  .option("--search <text>", "Fuzzy search (typos, word order, prefixes) over names, addresses, subjects and the text of every message in EVERY non-warmup conversation: all channels unless --channel narrows, archived and sent-only threads included. --segment, --unanswered and the Primary tab's negative-tier exclusion are ignored while set; explicit facets such as --status still narrow. For structured filters use --status or --tag, not a text search.")
14895
14905
  .option("--include-archived", "Include archived conversations.")
14896
14906
  .option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
@@ -14927,6 +14937,7 @@ Examples:
14927
14937
  ["domain", "domain"],
14928
14938
  ["excludeDomain", "exclude_domain"],
14929
14939
  ["mailboxId", "mailbox_id"],
14940
+ ["senderProfile", "sender_profile_id"],
14930
14941
  ["cursor", "cursor"],
14931
14942
  ]) {
14932
14943
  const value = readOption(options[flag]);
@@ -15109,8 +15120,9 @@ Examples:
15109
15120
  .option("--exclude-domain <domains>", "Email only: comma-separated counterpart domains to exclude.")
15110
15121
  .option("--mailbox-id <ids>", "Email only: comma-separated mailbox ids.")
15111
15122
  .option("--sender-account-id <ids>", "DMs only: comma-separated OXYGEN sender UUIDs from `oxygen senders list --json` (not LinkedIn member ids).")
15112
- .option("--since <iso>", "Only conversations whose last message is on/after this ISO date/timestamp. Cross-channel.")
15113
- .option("--until <iso>", "Only conversations whose last message is on/before this ISO date/timestamp. Cross-channel.")
15123
+ .option("--sender-profile <ids>", "Comma-separated sender profile ids (people, from `senders profiles list`): only their own inboxes and LinkedIn/WhatsApp accounts, as in `inbox list --sender-profile`.")
15124
+ .option("--since <iso>", "Only conversations whose last message is on/after this ISO date/timestamp. Excludes conversations without a last-message date. Cross-channel.")
15125
+ .option("--until <iso>", "Only conversations whose last message is on/before this ISO date/timestamp. Excludes conversations without a last-message date. Cross-channel.")
15114
15126
  .option("--search <text>", "Fuzzy search (typos, word order, prefixes) over names, addresses, subjects and the text of every message in EVERY non-warmup conversation: all channels unless --channel narrows, archived and sent-only threads included. --segment, --unanswered and the Primary tab's negative-tier exclusion are ignored while set; explicit facets such as --status still narrow.")
15115
15127
  .option("--include-archived", "Also mark archived conversations read.")
15116
15128
  .option("--unanswered", "Only sweep conversations you have not replied to (the last message is inbound) — the same filter as `inbox list --unanswered`, so the scope is exactly that list.")
@@ -15138,6 +15150,7 @@ Examples:
15138
15150
  ["excludeDomain", "exclude_domain"],
15139
15151
  ["mailboxId", "mailbox_id"],
15140
15152
  ["senderAccountId", "sender_account_id"],
15153
+ ["senderProfile", "sender_profile_id"],
15141
15154
  ["since", "since"],
15142
15155
  ["until", "until"],
15143
15156
  ["search", "search"],
@@ -15328,6 +15341,57 @@ Examples:
15328
15341
  });
15329
15342
  });
15330
15343
  }))
15344
+ .addCommand(new Command("views")
15345
+ .description("Saved Unibox views: named filter states the whole team shares (a sender, channels, statuses). Each member picks their own default, which the Unibox opens on.")
15346
+ .addCommand(new Command("list")
15347
+ .description("List the team's saved views; is_default marks your own default.")
15348
+ .option("--json", "Print a JSON envelope.")
15349
+ .action(async (options) => {
15350
+ await handleAsyncAction("inbox views list", options, () => requestOxygen("/api/cli/inbox/views"));
15351
+ }))
15352
+ .addCommand(new Command("create")
15353
+ .description("Save a view for the team.")
15354
+ .requiredOption("--name <name>", "View name, unique in the workspace (up to 80 characters).")
15355
+ .option("--filters <url-or-query>", "A Unibox URL or its query string, e.g. 'sender_profile_id=<id>&channels=linkedin&status=interested' (the `inbox list` params). Omit for every conversation.")
15356
+ .option("--default", "Also make it your own default view.")
15357
+ .option("--json", "Print a JSON envelope.")
15358
+ .action(async (options) => {
15359
+ await handleAsyncAction("inbox views create", options, () => requestOxygen("/api/cli/inbox/views", {
15360
+ method: "POST",
15361
+ body: {
15362
+ name: readOption(options.name),
15363
+ ...(readOption(options.filters) ? { filters: readOption(options.filters) } : {}),
15364
+ ...(options.default === true ? { default: true } : {}),
15365
+ },
15366
+ }));
15367
+ }))
15368
+ .addCommand(new Command("update")
15369
+ .description("Rename, re-filter or reorder a view, or set it as (or stop using it as) your own default.")
15370
+ .argument("<view>", "View id (from `inbox views list`).")
15371
+ .option("--name <name>", "New name.")
15372
+ .option("--filters <url-or-query>", "Replace the view's filters: a Unibox URL or its query string.")
15373
+ .option("--position <n>", "Zero-based order in the team's list.")
15374
+ .option("--default", "Make it your own default view.")
15375
+ .option("--no-default", "Stop using it as your default view (you open on the built-in view).")
15376
+ .option("--json", "Print a JSON envelope.")
15377
+ .action(async (view, options) => {
15378
+ await handleAsyncAction("inbox views update", options, () => requestOxygen(`/api/cli/inbox/views/${encodeURIComponent(view)}`, {
15379
+ method: "PATCH",
15380
+ body: {
15381
+ ...(readOption(options.name) ? { name: readOption(options.name) } : {}),
15382
+ ...(readOption(options.filters) !== null ? { filters: readOption(options.filters) } : {}),
15383
+ ...(readOption(options.position) !== null ? { position: readNonNegativeInt(options.position) } : {}),
15384
+ ...(typeof options.default === "boolean" ? { default: options.default } : {}),
15385
+ },
15386
+ }));
15387
+ }))
15388
+ .addCommand(new Command("delete")
15389
+ .description("Delete a view for the whole team. Anyone using it as their default opens on the built-in view.")
15390
+ .argument("<view>", "View id (from `inbox views list`).")
15391
+ .option("--json", "Print a JSON envelope.")
15392
+ .action(async (view, options) => {
15393
+ await handleAsyncAction("inbox views delete", options, () => requestOxygen(`/api/cli/inbox/views/${encodeURIComponent(view)}`, { method: "DELETE" }));
15394
+ })))
15331
15395
  .addCommand(new Command("labels")
15332
15396
  .description("Manage inbox status labels (the Instantly-style system set + custom labels).")
15333
15397
  .addCommand(new Command("list")
@@ -17574,7 +17638,22 @@ Examples:
17574
17638
  });
17575
17639
  })));
17576
17640
  program.addCommand(new Command("managed-inboxes")
17577
- .description("Managed sending inboxes bought through OXYGEN: subscribe a domain + N InboxKit mailboxes (google/microsoft/azure) as a recurring monthly Oxygen-credit purchase, add inboxes to a domain you already own, inspect/verify, and cancel. OXYGEN Warm-up defaults on when available: its quoted recurring credits and exact future addresses are part of the order approval, and the worker activates it after provisioning without a second warmup approval. Warm-up does not prove native-send authorization: verify each exact address with `mailboxes oauth-health --json`. Subscribe/add-inboxes preview first (re-run with --approved --quote); cancel previews first (re-run with --approved --confirm <domain>). To remove several domains with their inboxes, use `domains delete`. The inbox vendor is chosen for you; --vendor pins one.")
17641
+ .addCommand(new Command("recover")
17642
+ .description("Restore a cancelled managed subscription using its original addresses and domain. Find candidates with managed-inboxes list --status cancelled --json. Without --approved, performs a read-only preview at 0 credits: eligibility, upfront credits, recurring charges and a quote_id. Review the preview, then use --approved --quote <id> --max-credits <n>. Interrupted provider attempts are inspected before any repeat. After provider recovery, verify sending authorization and warmup for the exact recovered mailboxes. If warmup is disabled, preview enrollment and review its scope and costs before approving it.")
17643
+ .argument("<domain>", "The cancelled managed sending domain")
17644
+ .option("--approved", "Execute the reviewed recovery quote", false)
17645
+ .option("--quote <id>", "quote_id from the recovery preview")
17646
+ .option("--rebuy", "Include replacement licenses in the recovery preview and approval", false)
17647
+ .option("--max-credits <n>", "Upfront recovery credit ceiling; does not cap recurring charges", Number)
17648
+ .option("--json", "Print a JSON envelope")
17649
+ .action(async (domain, options) => {
17650
+ await handleAsyncAction("managed-inboxes recover", options, () => requestOxygen("/api/cli/managed-inboxes/recover", {
17651
+ method: "POST", body: { domain, approved: options.approved === true, rebuy: options.rebuy === true,
17652
+ ...(options.quote ? { quote_id: options.quote } : {}),
17653
+ ...(options.maxCredits !== undefined ? { max_credits: options.maxCredits } : {}) },
17654
+ }));
17655
+ }))
17656
+ .description("Managed sending inboxes bought through OXYGEN: subscribe a domain + N InboxKit mailboxes (google/microsoft/azure) as a recurring monthly Oxygen-credit purchase, add inboxes to a domain you already own, inspect/verify, cancel, and recover cancelled original addresses. OXYGEN Warm-up defaults on when available: its quoted recurring credits and exact future addresses are part of the order approval, and the worker activates it after provisioning without a second warmup approval. Warm-up does not prove native-send authorization: verify each exact address with `mailboxes oauth-health --json`. Subscribe/add-inboxes preview first (re-run with --approved --quote); cancel previews first (re-run with --approved --confirm <domain>). To remove several domains with their inboxes, use `domains delete`. The inbox vendor is chosen for you; --vendor pins one.")
17578
17657
  .addCommand(new Command("verify")
17579
17658
  .description("Check that OXYGEN, STRIPE, and the VENDOR agree about what this org is buying. The truth about a managed inbox lives in three systems — what the customer asked for, what they are charged, and what is actually running — and a 200 from any one of them proves nothing. Reports every disagreement with WHO IS LOSING MONEY while it stands (customer_overbilled first, then oxygen_pays). Read-only, no writes, 0 Oxygen credits.")
17580
17659
  .option("--json", "Print a JSON envelope.")
@@ -17738,7 +17817,7 @@ Examples:
17738
17817
  });
17739
17818
  }))
17740
17819
  .addCommand(new Command("cancel")
17741
- .description("Cancel a managed inbox order by domain: the inboxes AND every recurring add-on on them (warm-up, inbox placement). WITHOUT --approved prints the itemized teardown — every line that stops billing, when the inboxes go dark, and which live sequences send from them. To cancel, re-run with --approved AND --confirm <domain> echoing the domain exactly. Irreversible; the domain itself stays registered until its term ends. Cancelling is free.")
17820
+ .description("Cancel a managed inbox order by domain: the inboxes AND every recurring add-on on them (warm-up, inbox placement). WITHOUT --approved prints the itemized teardown — every line that stops billing, when the inboxes go dark, and which live sequences send from them. To cancel, re-run with --approved AND --confirm <domain> echoing the domain exactly. Once cancelled, original-address recovery depends on provider eligibility; preview with managed-inboxes recover. The domain stays registered until its term ends. Cancelling is free.")
17742
17821
  .argument("[domain]", "The managed inbox domain to cancel. May also be passed as --domain.")
17743
17822
  .option("--domain <domain>", "The managed inbox domain (alternative to the positional argument).")
17744
17823
  .option("--approved", "Actually cancel (otherwise the teardown preview is returned).")
@@ -25131,7 +25210,12 @@ async function handleWhoamiAction(options) {
25131
25210
  const cachedOrg = existingCredentials?.activeOrganization ?? cached?.organization ?? null;
25132
25211
  const orgChanged = !cachedOrg || cachedOrg.id !== identity.organization.id;
25133
25212
  const userChanged = !cached || cached.user.id !== identity.user.id;
25134
- if (existingCredentials && (orgChanged || userChanged)) {
25213
+ // Heal only from a request that ran as this profile. An environment token
25214
+ // replaces its token, and OXYGEN_ORG or --org picks a workspace for this
25215
+ // command only; saving what either resolved repointed the profile, and with
25216
+ // it every later unpinned command in every shell that shares it (2026-10-05).
25217
+ const pinnedElsewhere = Boolean(process.env.OXYGEN_ORG?.trim()) && orgChanged;
25218
+ if (existingCredentials && !readEnvToken() && !pinnedElsewhere && (orgChanged || userChanged)) {
25135
25219
  const refreshed = {
25136
25220
  token: existingCredentials.token,
25137
25221
  apiUrl: existingCredentials.apiUrl,
@@ -25610,9 +25694,16 @@ async function handleUpdateAction(options) {
25610
25694
  emitCliFailure("update", error);
25611
25695
  }
25612
25696
  }
25613
- /** `oxygen home`: the same outcome-neutral standup rendered by web Home. */
25614
- async function readHomeStandup() {
25615
- return requestOxygen("/api/cli/home/standup");
25697
+ /**
25698
+ * `oxygen home`: the outcome-neutral workspace standup. Only the plain view printed to
25699
+ * an interactive terminal is the person looking and marks Home as seen; `--json`, or
25700
+ * output an agent's shell or a pipe captures, sends `mark_seen=false` and leaves the
25701
+ * person's Home marker alone (founder decision, 2026-10-05; see home-standup.ts).
25702
+ */
25703
+ async function readHomeStandup(options) {
25704
+ return requestOxygen(homeStandupMarksSeen({ json: options.json, isTTY: process.stdout.isTTY })
25705
+ ? "/api/cli/home/standup"
25706
+ : "/api/cli/home/standup?mark_seen=false");
25616
25707
  }
25617
25708
  function buildApiKeyCreateBody(options) {
25618
25709
  const body = {};
@@ -25894,7 +25985,12 @@ async function promptForToken(options) {
25894
25985
  exitCode: 1,
25895
25986
  });
25896
25987
  }
25897
- if (!process.stdin.isTTY) {
25988
+ // The browser sign-in needs no terminal, so an agent's tool shell (Claude
25989
+ // Code, Codex, Cursor), which has no TTY, can still run the setup prompt's
25990
+ // `oxygen login` while the person approves in their browser. Only the pasted
25991
+ // token fallback reads stdin. CI can never approve, so it still fails at once.
25992
+ const interactive = process.stdin.isTTY === true;
25993
+ if (!interactive && (!options.browser || isTruthyEnv(process.env.CI))) {
25898
25994
  throw new OxygenError("missing_token", `Pass a CLI API token with --token, or run \`oxygen login\` in an interactive terminal. Create a token at ${settingsUrl}.`, {
25899
25995
  details: { login_url: fallbackLoginUrl },
25900
25996
  exitCode: 1,
@@ -25932,7 +26028,8 @@ async function promptForToken(options) {
25932
26028
  }
25933
26029
  catch (error) {
25934
26030
  spinner.fail(`Browser login unavailable: ${readErrorMessage(error)}`);
25935
- output.write(`Paste a CLI API token instead (create one at ${settingsUrl}).\n`);
26031
+ if (interactive)
26032
+ output.write(`Paste a CLI API token instead (create one at ${settingsUrl}).\n`);
25936
26033
  }
25937
26034
  finally {
25938
26035
  clearTimeout(hintTimer);
@@ -25940,6 +26037,12 @@ async function promptForToken(options) {
25940
26037
  await browserSession.close();
25941
26038
  }
25942
26039
  }
26040
+ if (!interactive) {
26041
+ throw new OxygenError("missing_token", `The browser sign-in did not complete. Run \`oxygen login\` again and approve it in your browser, or pass a CLI API token with --token. Create a token at ${settingsUrl}.`, {
26042
+ details: { login_url: fallbackLoginUrl },
26043
+ exitCode: 1,
26044
+ });
26045
+ }
25943
26046
  return promptForManualToken();
25944
26047
  }
25945
26048
  /**
@@ -5,7 +5,7 @@ import { requestOxygen } from "./http-client.js";
5
5
  /** Legacy no-flag invocation keeps the complete descriptor. */
6
6
  export function createToolCatalogDetailCommand(handle) {
7
7
  return addContractFlags(new Command("get")
8
- .description("Get native tool metadata. Full by default; explicit views report omissions and retain exact schemas. Executable MCP definitions use capabilities schema.")
8
+ .description("Read one provider operation's workspace-effective fee with --view pricing --json. Full metadata is the default; explicit views report omissions and retain exact schemas. Executable MCP definitions use capabilities schema.")
9
9
  .argument("<tool_id>", "Tool id.")
10
10
  .option("--json", "Print a JSON envelope."), toolCatalogDetailQueryContract)
11
11
  .action(async (toolId, options) => {
@@ -969,6 +969,30 @@ function crmEnrichmentReadRecommendation(query) {
969
969
  exactMcpCall: { tool: "oxygen_crm_enrichment", arguments: { action: "list", ...(omitCounts ? { backfill: false } : {}), projection: "summary" } },
970
970
  };
971
971
  }
972
+ /** CRM backfills and account projections use native Records repair owners. */
973
+ function crmMaintenanceRecommendation(query) {
974
+ if (isHostedWorkflowIntent(query)
975
+ || /\b(workflows?|agents?|functions?|callables?|recurring)\b/.test(query)
976
+ || isSecondPartyDecisionIntent(query)
977
+ || isTableMaintenanceIntent(query))
978
+ return null;
979
+ if (/\bcrm\b/.test(query) && /\benrich\w*\b/.test(query)
980
+ && !/\b(run|running|execute|paid)\b/.test(query)
981
+ && /\b(backfill|missing|empty|blank|unfilled)\b/.test(query))
982
+ return {
983
+ tools: ["oxygen_crm_enrichment"],
984
+ commands: ["crm enrichment backfill", "crm enrichment list"],
985
+ execution: "Inspect the configured companies/people enrichments and preview one bounded backfill batch. A blank field does not prove enrichment never ran. oxygen_crm_enrichment action=backfill uses oxygen_capabilities_call even for its free preview. Paid live batches require approval for the exact object, batch scope and max_credits; retain existing values and read the run outcome before another batch.",
986
+ };
987
+ if (/\b(open deal|account stage|lifecycle stage)\b/.test(query)
988
+ || (/\bcrm\b/.test(query) && /\bdeal stage\b/.test(query)))
989
+ return {
990
+ tools: ["oxygen_crm_rollup", "oxygen_crm_record_get"],
991
+ commands: ["crm rollup", "crm get"],
992
+ execution: "Open Deal is the most advanced open deal stage; it is blank when none remain. Account Stage is the company lifecycle derived from people and deals, with a separate human override. Inspect the records; oxygen_crm_rollup previews the selected company scope for 0 credits without provider calls. include_lifecycle adds Account Stage; live applies the reviewed repair only after approval. MCP uses oxygen_capabilities_call.",
993
+ };
994
+ return null;
995
+ }
972
996
  const CRM_ENRICHMENT_READ_VIEW = {
973
997
  owns: "Saved automatic-enrichment presets and batch caps for native CRM companies/people, backed by Tables auto-run.",
974
998
  notFor: "Running enrichment, provider catalog/sourcing, linked working Tables, external CRM sync, or changing the standing grant.",
@@ -979,6 +1003,9 @@ const CRM_ENRICHMENT_READ_VIEW = {
979
1003
  function routeCardFor(card, query) {
980
1004
  if (card.id === "records" && isCrmEnrichmentConfigurationRead(query))
981
1005
  return { ...card, ...CRM_ENRICHMENT_READ_VIEW };
1006
+ const crmMaintenance = card.id === "records" ? crmMaintenanceRecommendation(query) : null;
1007
+ if (crmMaintenance)
1008
+ card = { ...card, execution: crmMaintenance.execution };
982
1009
  const feed = card.primitive === "tables" ? tableFeedRecommendation(query) : null;
983
1010
  if (feed)
984
1011
  return { ...card, execution: feed.execution };
@@ -1513,6 +1540,10 @@ function tableMaintenanceRecommendation(query) {
1513
1540
  : result(["oxygen_table_action_item_resolve", "oxygen_table_runs_effect_unknown"], ["table-actions resolve", "table-runs effect-unknown"], "After verifying the external destination, record applied or not_applied by action item_id. This records an outcome without repeating the effect; retry remains a separate guarded action.");
1514
1541
  if (/\b(effect.*unknown|uncertain|verified outcome|blocked.*resolve)\b/.test(query))
1515
1542
  return result(["oxygen_table_runs_effect_unknown", "oxygen_table_runs_get", "oxygen_table_runs_items"], ["table-runs effect-unknown", "table-runs get", "table-runs items"], "Inspect exact effect-unknown items and verify the external destination. Only then record a verdict with oxygen_table_action_item_resolve by item_id or oxygen_table_cells_resolve by coordinates. Pending/failed items are not proof; retry is separate.");
1543
+ // Everything that ran on one Table (imports, column runs, writes) is the workspace run
1544
+ // ledger filtered by table; table-runs covers action runs only (blind CLI/MCP passes, 2026-10-05).
1545
+ if (/\b(every|all|already|ever|history|imports?|ingestions?|touched|anything|everything)\b/.test(query) && !/\b(failures?|failed|items?|cells?|effect)\b/.test(query))
1546
+ return result(["oxygen_runs_list", "oxygen_table_action_runs_list"], ["runs list", "table-runs list"], "List every recorded run of one Table, newest first, with `runs list --table <table>` (MCP oxygen_runs_list with table): imports, column runs and other writes. table-runs list covers column/action runs only.");
1516
1547
  return result(["oxygen_table_action_runs_list", "oxygen_table_runs_get", "oxygen_table_runs_items"], ["table-runs list", "table-runs get", "table-runs items"], "Find the Table's durable action run, then inspect its status and items. Pending, leased and failed are distinct from effect-unknown; do not record a verified verdict or retry an external effect without evidence.");
1517
1548
  }
1518
1549
  return null;
@@ -1584,6 +1615,8 @@ function explicitCapabilityIntent(query) {
1584
1615
  return ROUTE_BY_ID.get("connected-linkedin") ?? null;
1585
1616
  if (isCrmEnrichmentConfigurationRead(query))
1586
1617
  return ROUTE_BY_ID.get("records") ?? null;
1618
+ if (crmMaintenanceRecommendation(query))
1619
+ return ROUTE_BY_ID.get("records") ?? null;
1587
1620
  if (databaseRecommendation(query))
1588
1621
  return ROUTE_BY_ID.get("platform-operations") ?? null;
1589
1622
  if (oneShotContactFindingRecommendation(query))
@@ -1898,6 +1931,9 @@ function recommendationsFor(card, query) {
1898
1931
  return sent;
1899
1932
  if (card.id === "records" && isCrmEnrichmentConfigurationRead(query))
1900
1933
  return crmEnrichmentReadRecommendation(query);
1934
+ const crmMaintenance = card.id === "records" ? crmMaintenanceRecommendation(query) : null;
1935
+ if (crmMaintenance)
1936
+ return crmMaintenance;
1901
1937
  const database = card.id === "platform-operations" ? databaseRecommendation(query) : null;
1902
1938
  if (database)
1903
1939
  return database;
@@ -78,7 +78,7 @@ export const BROWSER_OPERATIONS = [
78
78
  operation("tables.resource.views.create", `${table}/views`, "POST", "tables views create", "oxygen_tables_views_create", "tables", "Create a saved Table view; group_by configures the same kanban grouping as the browser.", viewFields, ["name"], nativeCli),
79
79
  operation("tables.resource.views.edit", `${table}/views/{view}`, "PATCH", "tables views update", "oxygen_tables_views_update", "tables", "Edit a saved view's name, type, configuration, grouping, order or default status.", viewFields, [], nativeCli),
80
80
  operation("tables.resource.views.delete", `${table}/views/{view}`, "DELETE", "tables views delete", "oxygen_tables_views_delete", "tables", "Delete one saved Table view.", {}, [], nativeCli),
81
- operation("tables.resource.runs.list", `${table}/runs`, "GET", "tables runs history", "oxygen_table_runs_history", "runs", "Read this Table's durable run history and progress.", { status: enumOf(["all", "active", "queued", "running", "paused", "completed", "completed_with_errors", "failed", "canceling", "canceled"]), limit: int({ minimum: 1 }) }),
81
+ operation("tables.resource.runs.list", `${table}/runs`, "GET", "tables runs history", "oxygen_table_runs_history", "runs", "Read this Table's action-run history and progress.", { status: enumOf(["all", "active", "queued", "running", "paused", "completed", "completed_with_errors", "failed", "canceling", "canceled"]), limit: int({ minimum: 1 }) }),
82
82
  operation("tables.resource.runs.start", `${table}/runs`, "POST", "tables runs start", "oxygen_table_resource_run_start", "runs", "Start selected Table actions using the browser's exact row-selection contract. Paid runs require an approved max_credits ceiling.", { actions, selection, max_credits: num({ minimum: 0 }), max_concurrency: int({ minimum: 1 }), force: bool(), approved: bool() }, ["actions"], { spends: true }),
83
83
  operation("tables.resource.runs.estimate", `${table}/runs/estimate`, "POST", "tables runs preview", "oxygen_table_resource_run_preview", "runs", "Preview selected Table action costs and provider availability, including draft column definitions; starts no run.", { actions, selection }, ["actions"], { mutates: false }),
84
84
  operation("tables.resource.runs.cancel", `${run}/cancel`, "POST", "tables runs stop", "oxygen_table_resource_run_stop", "runs", "Cancel one durable Table run; never repeats external provider effects."),
@@ -80,8 +80,8 @@ export const CUSTOMER_OPERATIONS = [
80
80
  operation("spintax.generate", "/api/cli/spintax", "POST", "spintax generate", "oxygen_spintax_generate", "sequences", "Generate spintax for outreach text. Costs exactly 3 credits; failed generation refunds the reservation.", { text: str({ minLength: 1, maxLength: 4000 }), section: str({ maxLength: 80 }) }, ["text"], { spends: true }),
81
81
  operation("table_action_items.resolve", "/api/cli/table-action-items/{item_id}/resolve", "POST", "table-actions resolve", "oxygen_table_action_item_resolve", "tables", "Record a human-verified uncertain Table action outcome without repeating it. Use cells resolve for row/column coordinates.", TABLE_ITEM_RESOLVE_CONTRACT.properties, ["outcome"]),
82
82
  operation("table_ingestions.append", "/api/cli/table-ingestion-runs/{ingestion_run_id}/items", "POST", "table-ingestions append", "oxygen_table_ingestions_append_items", "runs", "Append bounded ingestion items to an existing run; preserves the run's approved scope.", { items: rows(), max_attempts: int({ minimum: 1 }) }, ["items"], cliOnly),
83
- operation("tables.favorites.get", "/api/cli/tables/favorite", "GET", "tables favorites", "oxygen_tables_favorites", "tables", "Page Table/folder stars (per-member preference; free (0 credits)); use next_cursor. Tool-catalog favorites are separate.", TABLE_FAVORITES_LIST_CONTRACT.properties, [], { generated: { cli: true, mcp: false } }),
84
- operation("tables.favorite.set", "/api/cli/tables/favorite", "POST", "tables favorite", "oxygen_tables_favorite", "tables", "Star/unstar exactly one table or folder; read back saved per-member preference. Free (0 credits). Tool-catalog favorites are separate.", TABLE_FAVORITE_CONTRACT.properties, ["favorite"]),
83
+ operation("tables.favorites.get", "/api/cli/tables/favorite", "GET", "tables favorites", "oxygen_tables_favorites", "tables", "Read starred Tables/folders (per-member preference; free (0 credits)); next_cursor. Catalog stars differ.", TABLE_FAVORITES_LIST_CONTRACT.properties, [], { generated: { cli: true, mcp: false } }),
84
+ operation("tables.favorite.set", "/api/cli/tables/favorite", "POST", "tables favorite", "oxygen_tables_favorite", "tables", "Star/unstar exactly one table or folder (saved per-member preference). Free (0 credits). Catalog stars differ.", TABLE_FAVORITE_CONTRACT.properties, ["favorite"]),
85
85
  operation("tables.import.csv", "/api/cli/tables/import-csv", "POST", "tables import-csv", "oxygen_tables_import_csv", "tables", "Import inline CSV into a hosted Table through its durable ingestion run. Use staged upload for large files.", { csv_text: text(), csv_base64: text(), table: text(), table_name: text(), project: text(), filename: text(), upsert_key: text(), batch_size: int({ minimum: 1, maximum: 5000 }) }, [], cliOnly),
86
86
  operation("tables.import.prepare", "/api/cli/tables/import-url", "POST", "tables import-upload", "oxygen_tables_import_upload", "tables", "Prepare a scoped storage upload for a hosted Table import; uploads and imports are separate, inspectable steps.", { file_name: text(), content_type: text(), byte_length: int({ minimum: 1 }), trace_id: text(), format: enumOf(["csv", "json", "xlsx"]) }, ["file_name", "byte_length"]),
87
87
  operation("tables.import.preview", "/api/cli/tables/import-preview", "POST", "tables import-preview", "oxygen_tables_import_preview", "tables", "Preview a staged file's headers, rows and existing-table mapping; writes no imported rows.", { storage_key: text(), file_name: text(), format: enumOf(["csv", "json", "xlsx"]), byte_length: int({ minimum: 1 }), table: text(), upsert_key: text() }, ["storage_key", "byte_length"], { mutates: false }),
@@ -25,6 +25,8 @@ function capacityProjection(value) {
25
25
  workspace_rows: reconciled ? usage.workspace_rows ?? null : null,
26
26
  largest_table_rows: reconciled ? usage.largest_table_rows ?? null : null,
27
27
  largest_table: reconciled ? usage.largest_table ?? null : null,
28
+ table_warning: reconciled ? usage.table_warning ?? null : null,
29
+ table_limit_reached: reconciled ? usage.table_limit_reached ?? null : null,
28
30
  } : null,
29
31
  note: "Rows are registry counts, available only after every Table is reconciled. Database bytes combine the last measured bytes and estimated subsequent growth; the estimate flag and timestamps name that state. Object storage is separate.",
30
32
  };
@@ -5,6 +5,15 @@ export declare const PUBLISHING_NOTE_LIST_LIMIT = 500;
5
5
  export declare const PUBLISHING_REVISION_LIST_LIMIT = 200;
6
6
  export declare const PUBLISHING_METRIC_LIST_LIMIT = 400;
7
7
  export declare const PUBLISHING_AMPLIFICATION_SCOPE_KINDS: readonly ["all", "tag", "post", "ugc_program", "ugc_peer_program"];
8
+ /** Exact stored GET branches of mixed Publishing owners. This is separate from
9
+ * no-spend previews: every omitted, malformed, write or future selector fails
10
+ * closed. Coarse tool annotations remain mixed; native identity/schema checks
11
+ * still run after this call-level classification. */
12
+ export declare const PUBLISHING_STORED_READ_SELECTORS: Readonly<Record<string, {
13
+ argument: string;
14
+ values: readonly string[];
15
+ }>>;
16
+ export declare function isPublishingStoredRead(name: string, args: Record<string, unknown> | null | undefined): boolean;
8
17
  export declare const publishingReadProjectionContract: ContractObject<{
9
18
  readonly projection: {
10
19
  readonly type: "string";
@@ -7,6 +7,25 @@ export const PUBLISHING_NOTE_LIST_LIMIT = 500;
7
7
  export const PUBLISHING_REVISION_LIST_LIMIT = 200;
8
8
  export const PUBLISHING_METRIC_LIST_LIMIT = 400;
9
9
  export const PUBLISHING_AMPLIFICATION_SCOPE_KINDS = ["all", "tag", "post", "ugc_program", "ugc_peer_program"];
10
+ /** Exact stored GET branches of mixed Publishing owners. This is separate from
11
+ * no-spend previews: every omitted, malformed, write or future selector fails
12
+ * closed. Coarse tool annotations remain mixed; native identity/schema checks
13
+ * still run after this call-level classification. */
14
+ export const PUBLISHING_STORED_READ_SELECTORS = {
15
+ oxygen_publishing_notes: { argument: "action", values: ["list"] },
16
+ oxygen_publishing_revisions: { argument: "action", values: ["list"] },
17
+ oxygen_publishing_analytics: { argument: "scope", values: ["summary", "timeseries", "post", "account"] },
18
+ oxygen_publishing_amplification: { argument: "action", values: ["list", "get", "actions"] },
19
+ };
20
+ export function isPublishingStoredRead(name, args) {
21
+ if (!args || typeof args !== "object" || Array.isArray(args))
22
+ return false;
23
+ const rule = Object.hasOwn(PUBLISHING_STORED_READ_SELECTORS, name) ? PUBLISHING_STORED_READ_SELECTORS[name] : undefined;
24
+ if (!rule || !Object.hasOwn(args, rule.argument))
25
+ return false;
26
+ const selector = args[rule.argument];
27
+ return typeof selector === "string" && rule.values.includes(selector);
28
+ }
10
29
  export const publishingReadProjectionContract = obj({
11
30
  projection: { ...str({ description: "full default; summary lists exact child metadata/indices; value streams lossless canonical JSON. Follow next_cursor with the same scope." }), enum: ["full", "summary", "value"] },
12
31
  value_path: str({ description: "Projection RFC6901 pointer: empty=root; /revisions/0/content_text, /comments/0/body, /series, /curve, /benchmarks or /accounts. No field is silently trimmed.", maxLength: 4096 }),
@@ -1,5 +1,6 @@
1
1
  export declare const MAX_TABLE_RECOVERY_ITEMS = 1000;
2
2
  export declare const TABLE_FAVORITES_LIST_CONTRACT: import("./schema.js").ContractObject<{
3
+ readonly details: import("./schema.js").BooleanProperty;
3
4
  readonly limit: {
4
5
  type: "integer";
5
6
  description?: string;
@@ -3,12 +3,13 @@ import { bool, enumOf, int, jsonValue, num, obj, str } from "./schema.js";
3
3
  const table = str({ minLength: 1, description: "Table id, slug, or Oxygen table URL." });
4
4
  export const MAX_TABLE_RECOVERY_ITEMS = 1000;
5
5
  export const TABLE_FAVORITES_LIST_CONTRACT = obj({
6
- limit: int({ minimum: 1, maximum: 200, description: "Page size; MCP default 20." }),
7
- cursor: str({ maxLength: 512, description: "Previous next_cursor; restart if stars changed." }),
8
- max_chars: int({ minimum: 500, maximum: 10_000, description: "Complete response cap in characters." }),
6
+ details: bool({ description: "Kind, id, name and web_url." }),
7
+ limit: int({ minimum: 1, maximum: 200, description: "Page size (default 20)." }),
8
+ cursor: str({ maxLength: 512, description: "next_cursor; restart if stars change." }),
9
+ max_chars: int({ minimum: 500, maximum: 10_000, description: "Response cap." }),
9
10
  });
10
11
  export const TABLE_FAVORITE_CONTRACT = obj({
11
- table: str({ minLength: 1, description: "Table id, slug, or Oxygen table URL. Pass exactly one of table or folder." }),
12
+ table: str({ minLength: 1, description: "Table id, slug or same-workspace URL. Pass exactly one of table or folder." }),
12
13
  folder: str({ minLength: 1, description: "Folder id or slug. Pass exactly one of table or folder." }),
13
14
  favorite: bool({ description: "true stars; false unstars." }),
14
15
  }, { required: ["favorite"] });
@@ -82,6 +82,8 @@ export type PlanLimitsStorageCapacity = {
82
82
  id?: string;
83
83
  name?: string;
84
84
  } | null;
85
+ table_warning?: boolean | null;
86
+ table_limit_reached?: boolean | null;
85
87
  workspace_database_bytes?: number;
86
88
  } | null;
87
89
  };
@@ -177,6 +179,8 @@ export declare function planLimitsLadder(payload: PlanLimitsEnvelope): PlanLimit
177
179
  * named, so a full one is identifiable (decision F.2).
178
180
  */
179
181
  export declare function planLimitsStorageUsageLine(storage: PlanLimitsStorageCapacity | undefined): string | null;
182
+ /** Status of the largest active Table, from the same flags CLI and MCP read. */
183
+ export declare function planLimitsTableStatus(storage: PlanLimitsStorageCapacity | undefined): "full" | "nearly full" | null;
180
184
  /** Sending limits are the same on every plan: one row per channel. */
181
185
  export declare function planLimitsSendingRows(sending: PlanLimitsSendingLimits | undefined): Array<{
182
186
  label: string;
@@ -201,13 +201,22 @@ export function planLimitsStorageUsageLine(storage) {
201
201
  if (!guard || !usage)
202
202
  return null;
203
203
  const largest = usage.largest_table?.name ? `Largest table “${usage.largest_table.name}”` : "Largest table";
204
+ const tableStatus = planLimitsTableStatus(storage);
204
205
  const parts = [
205
- `${largest} ${formatLimitCount(usage.largest_table_rows)} / ${formatLimitCount(guard.table_row_limit)} rows`,
206
+ `${largest} ${formatLimitCount(usage.largest_table_rows)} / ${formatLimitCount(guard.table_row_limit)} rows${tableStatus ? ` (${tableStatus})` : ""}`,
206
207
  `workspace ${formatLimitCount(usage.workspace_rows)} / ${formatLimitCount(guard.workspace_row_limit)} rows`,
207
208
  `database ${formatLimitBytes(usage.workspace_database_bytes)} / ${formatLimitBytes(guard.workspace_database_limit_bytes)}`,
208
209
  ];
209
210
  return `Your storage: ${parts.join(" · ")}`;
210
211
  }
212
+ /** Status of the largest active Table, from the same flags CLI and MCP read. */
213
+ export function planLimitsTableStatus(storage) {
214
+ if (storage?.usage?.table_limit_reached === true)
215
+ return "full";
216
+ if (storage?.usage?.table_warning === true)
217
+ return "nearly full";
218
+ return null;
219
+ }
211
220
  /** Sending limits are the same on every plan: one row per channel. */
212
221
  export function planLimitsSendingRows(sending) {
213
222
  if (!sending)
@@ -1,5 +1,13 @@
1
1
  import type { PlanBand } from "./plan-band.js";
2
2
  import { type RepricingOptions } from "./repricing.js";
3
+ /** Row capacity warnings start at 80% of the limit currently enforced. */
4
+ export declare const TABLE_ROW_CAPACITY_WARNING_RATIO = 0.8;
5
+ export type TableRowCapacityFlags = {
6
+ /** Null until the row count is reconciled. Also true at or above the hard limit. */
7
+ tableWarning: boolean | null;
8
+ tableLimitReached: boolean | null;
9
+ };
10
+ export declare function tableRowCapacityFlags(rows: number | null, rowLimit: number): TableRowCapacityFlags;
3
11
  export type WorkspaceTableCapacityLimits = {
4
12
  tableRowLimit: number;
5
13
  workspaceRowLimit: number;
@@ -1,5 +1,13 @@
1
1
  import { findRepricedValue, readRepricingSwitch, repricedValue, } from "./repricing.js";
2
2
  const GIB = 1024 ** 3;
3
+ /** Row capacity warnings start at 80% of the limit currently enforced. */
4
+ export const TABLE_ROW_CAPACITY_WARNING_RATIO = 0.8;
5
+ export function tableRowCapacityFlags(rows, rowLimit) {
6
+ return {
7
+ tableWarning: rows === null ? null : rows >= rowLimit * TABLE_ROW_CAPACITY_WARNING_RATIO,
8
+ tableLimitReached: rows === null ? null : rows >= rowLimit,
9
+ };
10
+ }
3
11
  /**
4
12
  * The envelope every workspace had before storage scaled with plan size, and
5
13
  * still the value in force for any workspace whose plan-resolved limits have not
@@ -1 +1 @@
1
- export declare const OXYGEN_BUILD_VERSION = "1.1011.804";
1
+ export declare const OXYGEN_BUILD_VERSION = "1.1011.855";
@@ -1,2 +1,2 @@
1
1
  // GENERATED by scripts/ci/version-stamp.mjs from VERSION + git history. Do not edit; do not commit.
2
- export const OXYGEN_BUILD_VERSION = "1.1011.804";
2
+ export const OXYGEN_BUILD_VERSION = "1.1011.855";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.1011.804",
3
+ "version": "1.1011.855",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",