@oxygen-agent/cli 1.275.1 → 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.
@@ -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
  /**
@@ -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.275.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.275.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oxygen-agent/cli",
3
- "version": "1.275.1",
3
+ "version": "1.284.3",
4
4
  "private": false,
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",