@oxygen-agent/cli 1.887.5 → 1.893.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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.887.5
37
+ Version: 1.893.0
@@ -39,7 +39,17 @@ const MUTATING_VERBS = new Set([
39
39
  // Exact leaves, not the `billing` prefix — `orgs billing-owners` is a read.
40
40
  // Both move a workspace's billing owner, so an agent must treat them as writes.
41
41
  "billing-link", "billing-unlink",
42
- "bind", "buy", "call", "cancel", "chat-action", "claim", "clear",
42
+ "bind",
43
+ // `knowledge bootstrap --live` researches the workspace's own company and
44
+ // WRITES the typed company profile plus a cited wiki page, on paid provider
45
+ // and AI calls. Its bare form previews (see PREVIEW_BY_DEFAULT via --live),
46
+ // which the rule above already covers: an approval-gated command is a
47
+ // mutation. `knowledge bootstrap status` keeps the leaf verb `status` and
48
+ // stays a read.
49
+ "bootstrap",
50
+ // `support chat` starts or continues a customer-authored Plain Thread. The
51
+ // conversational noun is still a real external write.
52
+ "buy", "call", "cancel", "chat", "chat-action", "claim", "clear",
43
53
  "comment", "configure", "connect", "create", "decide", "delete", "delist", "disable",
44
54
  "disconnect", "dispatch", "done", "draft", "duplicate", "edit", "emit", "enable", "enroll",
45
55
  // Exact leaf, not a `field` prefix: `support admin field-set` writes a Plain
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ import { fileURLToPath, pathToFileURL } from "node:url";
9
9
  import { Command, CommanderError, Option } from "commander";
10
10
  import { applyOxygenHelp } from "./help.js";
11
11
  import { buildCommandManifest, getCommandManifestEntry, searchCommandManifest, suggestCommandNames, } from "./command-manifest.js";
12
- import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TABLE_IMPORT_ROW_LIMIT, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
12
+ import { AGENCY_DIRECTORY_REGIONS, AGENCY_DIRECTORY_SERVICES, COLLAB_GATE_KINDS, COLLAB_GATE_PROSE, COLLAB_SUBJECT_KINDS, COLLAB_SUBJECT_KINDS_PROSE, COLLAB_SUBJECT_LABELS, COLLAB_SUBJECT_PROSE, GATE_KIND_SUBJECTS, describeWorkflowStatusChange, formatCellForDisplay, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_MCP_TOOL_NAME_LENGTH, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TABLE_IMPORT_ROW_LIMIT, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
13
13
  import { TAG_COLORS } from "@oxygen/shared/select-options";
14
14
  import { inferImportColumnLabels, inferRowsFileFormat, normalizeImportColumnKey, normalizeRowsForNewTable, normalizeRowsFormat, parseRowsFileBuffer, parseXlsxWorkbookBuffer, } from "@oxygen/shared/file-import";
15
15
  import { MAILBOX_IMPORT_FILE_MAX_BYTES as SHARED_MAILBOX_IMPORT_FILE_MAX_BYTES, MAILBOX_IMPORT_ROW_LIMIT as SHARED_MAILBOX_IMPORT_ROW_LIMIT, normalizeMailboxImportFile as normalizeSharedMailboxImportFile, normalizeMailboxImportVendor as normalizeSharedMailboxImportVendor, normalizeMailboxWorkbookRows, parseMailboxImportText, summarizeMailboxImportValidation as summarizeSharedMailboxImportValidation, } from "@oxygen/shared/mailbox-import";
@@ -2720,6 +2720,7 @@ export function createProgram() {
2720
2720
  query,
2721
2721
  route: serializeCapabilityRoute(inferUserCapabilityRoute(query)),
2722
2722
  hint: "Hydrate one exact CLI command with `oxygen commands get <exact-command> --json`, or one MCP tool with `oxygen_capabilities_schema`.",
2723
+ alternate_search_hint: `If this route does not fit the outcome, run \`oxygen skills search ${JSON.stringify(query)} --json\` for a narrower operational play.`,
2723
2724
  };
2724
2725
  });
2725
2726
  }))
@@ -2866,9 +2867,40 @@ export function createProgram() {
2866
2867
  }));
2867
2868
  program
2868
2869
  .command("support")
2869
- .description("Open and track Plain support conversations for the active OXYGEN organization. In the app, the support chat is the widget in the bottom-right corner of every page. Retry is the only recovery control; it returns after each failed connection attempt and never creates a Thread. CLI filing is a zero-credit write to the canonical Plain queue. Guide: https://oxygen-agent.com/docs/surfaces/support.")
2870
+ .description("Open and track Plain support conversations for the active OXYGEN organization. In the app, the support chat is the widget in the bottom-right corner of every page. Retry is the only recovery control; it returns after each failed connection attempt and never creates a Thread. `support chat` starts or continues a zero-credit conversation without making you write a ticket title. Guide: https://oxygen-agent.com/docs/surfaces/support.")
2871
+ .addCommand(new Command("chat")
2872
+ .description("Start a Plain Chat conversation from one message, or pass an existing Thread ID to continue it. New conversations derive Plain's internal title from the message; you never have to file each message as a separate titled ticket.")
2873
+ .argument("[threadId]", "Existing Plain Thread ID (th_...) to continue. Omit to start a new conversation.")
2874
+ .requiredOption("-m, --message <message>", "Your message to OXYGEN support.")
2875
+ .option("--severity <severity>", "New conversation only: low | normal | high. Defaults to normal.")
2876
+ .option("--category <category>", "New conversation only: question, bug, feature_request, billing, security_data, configuration, agency_directory, or other. Defaults to question.")
2877
+ .option("--json", "Print a JSON envelope.")
2878
+ .addHelpText("after", `
2879
+ Examples:
2880
+ oxygen support chat --message "The dedicated IP is not being assigned" --category bug
2881
+ oxygen support chat th_... --message "Here is the additional evidence"
2882
+ `)
2883
+ .action(async (threadId, options) => {
2884
+ const message = readOption(options.message);
2885
+ if (threadId) {
2886
+ await handleAsyncAction("support chat", options, () => requestOxygen(`/api/cli/support/tickets/${encodeURIComponent(threadId)}/messages`, {
2887
+ method: "POST",
2888
+ body: { body: message },
2889
+ }));
2890
+ return;
2891
+ }
2892
+ const severity = readOption(options.severity);
2893
+ await handleAsyncAction("support chat", options, () => requestOxygen("/api/cli/support/tickets", {
2894
+ method: "POST",
2895
+ body: {
2896
+ message,
2897
+ category: readOption(options.category) ?? "question",
2898
+ ...(severity ? { severity } : {}),
2899
+ },
2900
+ }));
2901
+ }))
2870
2902
  .addCommand(new Command("file")
2871
- .description("Create a real, zero-credit Plain support Thread immediately. This is a separate filing action, not the in-app Retry, and can create a second Thread: check `support list --status open` first and reply to the existing Thread for the same issue. There is no preview: review the exact subject, body, category, and severity before running it. Use when you're stuck on an OXYGEN operation.")
2903
+ .description("Create a structured, titled Plain support Thread immediately. Prefer `support chat` for an ordinary conversation; use this zero-credit filing path when explicit subject, operation, error, run, or table metadata helps triage. Check `support list --status open` first because a new filing can create a second Thread.")
2872
2904
  .requiredOption("--subject <subject>", "One-line summary of the problem.")
2873
2905
  .option("--body <body>", "What you were doing, what happened, and what you tried.")
2874
2906
  .option("--severity <severity>", "low | normal | high. Defaults to normal.")
@@ -3224,7 +3256,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
3224
3256
  }), { hidden: true }));
3225
3257
  program
3226
3258
  .command("feedback")
3227
- .description("Send feedback or a bug report to the OXYGEN team. This is the same immediate, zero-credit canonical Plain Thread write as `support file`; transcripts are never attached unless explicitly requested.")
3259
+ .description("Send feedback or a bug report to the OXYGEN team. This is the same immediate, zero-credit canonical Plain conversation write as `support chat`; transcripts are never attached unless explicitly requested.")
3228
3260
  .option("-m, --message <message>", "Your feedback or bug report.")
3229
3261
  .option("--severity <severity>", "low | normal | high. Defaults to normal.")
3230
3262
  .option("--category <category>", "Optional category label. Defaults to 'feedback'.")
@@ -6022,6 +6054,51 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6022
6054
  body: { id: assetId },
6023
6055
  }));
6024
6056
  })));
6057
+ // `knowledge bootstrap` is declared here, not inline in the group chain,
6058
+ // because it is a command that both ACTS and owns a subcommand, and both
6059
+ // levels take --json. Commander resolves a repeated option to the outer
6060
+ // command, so `knowledge bootstrap status --json` sets the flag on THIS
6061
+ // command, not on `status` — the same collision the `commands` group above
6062
+ // handles. The named reference lets `status` read the parent's flag back.
6063
+ const knowledgeBootstrapCommand = new Command("bootstrap")
6064
+ .description(`Research THIS workspace's own company from its domain and fill the typed company profile plus one cited wiki page — the empty-Knowledge-Graph fix, so a founder never has to type their own positioning back at us. Two authorization paths, and they are NOT the same: running this command manually is authorized by --live (plus --max-credits), like every other paid Oxygen action; the identical pass that runs ONCE automatically after signup is authorized by the consent recorded at signup instead, and is capped (<= ${KNOWLEDGE_BOOTSTRAP_MAX_CREDITS} credits), journalled (a marker, a system run, and immutable page sources), kill-switched fleet-wide, and reversible. A bare call previews: it resolves the domain, plans the reads, estimates the cost, and spends nothing. Once per workspace ever — any existing marker (completed, partial, skipped, failed) blocks a second pass until --force clears it.`)
6065
+ .option("--domain <domain>", "Company apex domain to research. Defaults to the workspace domain resolved at signup from the org's Clerk metadata or the creator's email; a personal-email signup has none, and a preview says so instead of guessing.")
6066
+ .option("--linkedin <url>", "Company LinkedIn URL, as a second identity signal alongside the domain.")
6067
+ .option("--live", "Authorize this manual run to spend and write. Requires --max-credits. Without it the command previews and nothing is billed.")
6068
+ .option("--approved", "Same as --live. Accepted because the global help footer names --approved as the way to authorize a credit-spending command.")
6069
+ .option("--max-credits <credits>", `Hard credit ceiling for the whole pass. Required with --live, and clamped server-side to ${KNOWLEDGE_BOOTSTRAP_MAX_CREDITS}.`)
6070
+ .option("--force", "Clear an existing once-ever marker first and re-arm the pass. This is the one deliberate way to research the same workspace twice — pair it with --live to actually spend again.")
6071
+ .option("--json", "Print a JSON envelope.")
6072
+ .action(async (options) => {
6073
+ await handleAsyncAction("knowledge bootstrap", options, () => requestOxygen("/api/cli/knowledge/bootstrap", {
6074
+ method: "POST",
6075
+ body: {
6076
+ mode: options.live || options.approved ? "live" : "dry_run",
6077
+ ...(readOption(options.domain) ? { domain: readOption(options.domain) } : {}),
6078
+ ...(readOption(options.linkedin) ? { linkedin_url: readOption(options.linkedin) } : {}),
6079
+ ...(readPositiveNumber(options.maxCredits) !== undefined
6080
+ ? { max_credits: readPositiveNumber(options.maxCredits) }
6081
+ : {}),
6082
+ ...(options.force ? { force: true } : {}),
6083
+ },
6084
+ }));
6085
+ })
6086
+ .addCommand(new Command("status")
6087
+ .description("Report the once-ever bootstrap marker for this workspace: status (queued, running, completed, partial, skipped, failed, or never attempted), when it ran, its system run id, the credits it actually spent, the domain researched, the reason it stopped, and the pages it wrote. Free, reads only, and the honest way to find out whether the automatic pass already happened before you spend on a manual one.")
6088
+ .option("--json", "Print a JSON envelope.")
6089
+ .action(async (options) => {
6090
+ // Commander hands a repeated --json to the outer command, so read the
6091
+ // parent's flag back before deciding the output shape. Without this a
6092
+ // machine caller asking for an envelope silently gets bare JSON.
6093
+ const outputOptions = {
6094
+ ...options,
6095
+ json: options.json || Boolean(knowledgeBootstrapCommand.opts().json),
6096
+ };
6097
+ await handleAsyncAction("knowledge bootstrap status", outputOptions, () => requestOxygen("/api/cli/knowledge/bootstrap", {
6098
+ method: "POST",
6099
+ body: { status: true },
6100
+ }));
6101
+ }));
6025
6102
  program
6026
6103
  .command("knowledge")
6027
6104
  .description("Company knowledge wiki: slug-addressed pages, a [[wikilink]] graph, and a decision log.")
@@ -6181,7 +6258,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6181
6258
  }));
6182
6259
  }))
6183
6260
  .addCommand(new Command("lint")
6184
- .description("Structural wiki health report: orphans, dead-ends, unresolved links, missing canonicals, untagged pages, duplicate titles, oversized hubs, unfilled seed stubs, decisions missing a reversal condition, stale/oversized pages, and aged proposals.")
6261
+ .description("Structural wiki health report: orphans (unfilled seed stubs excluded), dead-ends, unresolved links, contradictions (disputed pages, conflicting trust tags, competing live-copy pages), missing canonicals, untagged pages, duplicate titles, oversized hubs, unfilled seed stubs (total plus the most-linked-to), researchable stubs, decisions missing a reversal condition, stale pages (untouched or superseded by newer sources), oversized bodies, and aged proposals.")
6185
6262
  .option("--json", "Print a JSON envelope.")
6186
6263
  .action(async (options) => {
6187
6264
  await handleAsyncAction("knowledge lint", options, () => requestOxygen("/api/cli/knowledge/lint"));
@@ -6298,7 +6375,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6298
6375
  }));
6299
6376
  })))
6300
6377
  .addCommand(new Command("seed")
6301
- .description("Seed or upgrade the knowledge wiki scaffold: on first run creates the starter page set — a start-here guide, a conventions schema page, and stub hubs (icp, positioning, offers, competitors, playbooks, campaign-learnings, decisions), all [[wikilinked]]. Idempotent; never overwrites your edits.")
6378
+ .description("Seed or upgrade the knowledge wiki scaffold: on first run creates the starter page set — a start-here guide, a conventions schema page, and 19 draft stub hubs (icp, positioning, offers, competitors, playbooks, campaign-learnings, decisions, voice, brand, messaging, objections, playbook-outbound, playbook-inbound, playbook-nurture, qualification, personas, customers, research, metrics), all [[wikilinked]]. Idempotent; never overwrites your edits.")
6302
6379
  .option("--json", "Print a JSON envelope.")
6303
6380
  .action(async (options) => {
6304
6381
  await handleAsyncAction("knowledge seed", options, () => requestOxygen("/api/cli/knowledge/seed", {
@@ -6306,6 +6383,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
6306
6383
  body: {},
6307
6384
  }));
6308
6385
  }))
6386
+ .addCommand(knowledgeBootstrapCommand)
6309
6387
  .addCommand(new Command("synthesize")
6310
6388
  .description("Synthesize knowledge from workspace evidence (one low-cost AI call): campaign learnings from auto-filed outreach outcomes, or a voice guide from sent messages. Previews by default; --approved writes the result.")
6311
6389
  .option("--kind <kind>", "What to synthesize: campaign_learnings (default) or voice.", "campaign_learnings")
@@ -9293,7 +9371,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
9293
9371
  .requiredOption("--instructions <text>", "Workspace Agent instructions for this version.")
9294
9372
  .option("--model-policy-json <json>", AGENT_MODEL_POLICY_HELP)
9295
9373
  .option("--tool-policy-json <json>", AGENT_TOOL_POLICY_HELP)
9296
- .option("--context-policy-json <json>", "Context policy JSON: on_demand or prefetch, optional resolve_args and file_ids.")
9374
+ .option("--context-policy-json <json>", "Context policy JSON: on_demand, prefetch, or grounded (seeds the run from the workspace knowledge wiki); optional resolve_args and file_ids.")
9297
9375
  .option("--skills <csv>", "Comma-separated product skill slugs snapshotted into this immutable version.")
9298
9376
  .option("--sandbox-policy-json <json>", "Sandbox policy JSON.")
9299
9377
  .option("--limits-json <json>", "Optional limits JSON; platform hard ceilings still apply.")
@@ -9322,7 +9400,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
9322
9400
  .option("--description <text>", "Optional one-line description of the agent's job.")
9323
9401
  .option("--model-policy-json <json>", AGENT_MODEL_POLICY_HELP)
9324
9402
  .option("--tool-policy-json <json>", AGENT_TOOL_POLICY_HELP)
9325
- .option("--context-policy-json <json>", "Context policy JSON: on_demand or prefetch, optional resolve_args and file_ids.")
9403
+ .option("--context-policy-json <json>", "Context policy JSON: on_demand, prefetch, or grounded (seeds the run from the workspace knowledge wiki); optional resolve_args and file_ids.")
9326
9404
  .option("--skills <csv>", "Comma-separated product skill slugs snapshotted into this immutable version.")
9327
9405
  .option("--sandbox-policy-json <json>", "Sandbox policy JSON.")
9328
9406
  .option("--limits-json <json>", "Optional limits JSON; platform hard ceilings still apply.")
@@ -12115,17 +12193,21 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12115
12193
  program.addCommand(new Command("sequences")
12116
12194
  .description("Multichannel outreach sequences: one enrollment per lead can span LinkedIn, email, WhatsApp, human call tasks, and connected-CRM tasks. LinkedIn steps dispatch natively (rate-limited, credit-capped); email steps send natively or place/move/stop the lead in a bound Instantly campaign (BYOK); crm_task can exact-match/upsert/link CRM records and create a mapped external follow-up (HubSpot first), with replay-safe per-operation receipts. Cross-channel reply-stop is intrinsic.")
12117
12195
  .addCommand(new Command("list")
12118
- .description("List sequences with status, channels, tags, and credit usage. --stats joins each row's LIFETIME funnel (enrolled, sent, replies, reply rate, email opens/clicks, opportunities) — the same shape `sequences stats` returns for one sequence.")
12196
+ .description("List sequences with persisted launch status kept separate from current operational state. Fleet counts report marked-active versus operationally-live (active with nonterminal work), and rows explain flowing, paced, blocked, degraded, drained, or empty work without SQL. --stats joins each row's LIFETIME funnel.")
12119
12197
  .option("--status <status>", "Filter by status: draft, active, paused, or archived.")
12198
+ .addOption(new Option("--operational-state <state>", "Filter by current work state; fleet counts remain unfiltered: empty, drained, blocked, degraded, flowing, or paced.").choices(["empty", "drained", "blocked", "degraded", "flowing", "paced"]))
12120
12199
  .option("--tag <tag>", "Only sequences carrying this workspace tag (see `oxygen tags list`).")
12121
12200
  .option("--stats", "Attach lifetime stats per sequence (a stats pass per row — slower on large workspaces).")
12122
12201
  .option("--json", "Print a JSON envelope.")
12123
12202
  .action(async (options) => {
12124
- await handleAsyncAction("sequences list", options, () => {
12203
+ await handleSequenceReadAction("sequences list", options, () => {
12125
12204
  const params = new URLSearchParams();
12126
12205
  const status = readOption(options.status);
12127
12206
  if (status)
12128
12207
  params.set("status", status);
12208
+ const operationalState = readOption(options.operationalState);
12209
+ if (operationalState)
12210
+ params.set("operational_state", operationalState);
12129
12211
  const tag = readOption(options.tag);
12130
12212
  if (tag)
12131
12213
  params.set("tag", tag);
@@ -12133,7 +12215,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12133
12215
  params.set("include", "stats");
12134
12216
  const suffix = params.toString();
12135
12217
  return requestOxygen(`/api/cli/sequences${suffix ? `?${suffix}` : ""}`);
12136
- });
12218
+ }, formatSequenceListHealth);
12137
12219
  }))
12138
12220
  .addCommand(new Command("send")
12139
12221
  .description("Initiate one net-new LinkedIn message through a one-recipient, one-step hosted Sequence. First call without a sequence id: pass --recipient, --text/--text-file, and --sender to create the inert draft and preview (no message, no credits). After showing that preview, re-run with the returned sequence id plus --approved --max-credits N. Existing-thread replies stay in `oxygen inbox send`.")
@@ -12349,14 +12431,14 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12349
12431
  });
12350
12432
  })))
12351
12433
  .addCommand(new Command("analytics")
12352
- .description("Show organization-level sequencer analytics, per-sequence funnels, and native email attribution under analytics.emailAttribution: byMailbox, byDomain, and explicit unattributed facts. With --sequence, analytics.senderPerformance groups channel-conditional counts and rates by sending person; analytics.messagePerformance reports each copy-bearing deterministic step variant, target/actual distribution, contacted recipients, and type-specific accepted/reply/positive outcomes (absent for note-less connect-only campaigns). Message-level reply outcomes require preserved step/variant provenance, so older unattributed replies remain in campaign totals without being guessed into a message row. Provider-owned campaigns appear only after their telemetry is normalized into Oxygen's native Sequence ledgers. Also reports whether the reply → CRM automation is armed, with a link to the workflow.")
12434
+ .description("Show selected-window sequencer analytics with persisted status kept separate from operational state, fleet marked-active versus operationally-live counts, current open work, sent/terminal-failed/deferral totals, failure classes, and action-kind breakdowns. Also includes per-sequence funnels and native email attribution under analytics.emailAttribution: byMailbox, byDomain, and explicit unattributed facts. With --sequence, analytics.senderPerformance groups channel-conditional counts and rates by sending person; analytics.messagePerformance reports each copy-bearing deterministic step variant, target/actual distribution, contacted recipients, and type-specific accepted/reply/positive outcomes (absent for note-less connect-only campaigns). Message-level reply outcomes require preserved step/variant provenance, so older unattributed replies remain in campaign totals without being guessed into a message row. Provider-owned campaigns appear only after their telemetry is normalized into Oxygen's native Sequence ledgers. Also reports whether the reply → CRM automation is armed, with a link to the workflow.")
12353
12435
  .option("--range <range>", "Preset range: 7d, 14d, 28d, 30d, 90d, 180d, 365d, or all. Defaults to 14d. `all` starts at the oldest campaign in scope.")
12354
12436
  .option("--from <date>", "Custom start date (YYYY-MM-DD). Windows up to five years are accepted.")
12355
12437
  .option("--to <date>", "Custom end date (YYYY-MM-DD).")
12356
12438
  .option("--sequence <id-or-slug>", "Limit analytics to one sequence.")
12357
12439
  .option("--json", "Print a JSON envelope.")
12358
12440
  .action(async (options) => {
12359
- await handleAsyncAction("sequences analytics", options, () => {
12441
+ await handleSequenceReadAction("sequences analytics", options, () => {
12360
12442
  const params = new URLSearchParams();
12361
12443
  const range = readOption(options.range);
12362
12444
  const from = readOption(options.from);
@@ -12372,7 +12454,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12372
12454
  params.set("sequence", sequence);
12373
12455
  const qs = params.toString();
12374
12456
  return requestOxygen(`/api/cli/sequences/analytics${qs ? `?${qs}` : ""}`);
12375
- });
12457
+ }, formatSequenceAnalyticsHealth);
12376
12458
  }))
12377
12459
  .addCommand(new Command("create")
12378
12460
  .description("Create a draft multichannel sequence from a steps JSON file. Supports LinkedIn, email, WhatsApp, human call_task, and connected-CRM crm_task journeys. Assign LinkedIn senders with --senders (optional at create — a draft can sit senderless, but enroll/start require at least one for LinkedIn journeys); bind an Instantly email track with --email-*. Install/read the oxygen-sequencer skill for complete mapped CRM task examples.")
@@ -12821,11 +12903,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
12821
12903
  });
12822
12904
  }))
12823
12905
  .addCommand(new Command("stats")
12824
- .description("Show the sequence funnel: enrolled, invites sent, connected, replied, with acceptance and reply rates.")
12906
+ .description("Show one sequence's persisted launch status and current operational state separately, including reasons, open work, lifetime sent/terminal-failed/deferral counts, and the funnel.")
12825
12907
  .argument("<sequence>", "Sequence id or slug.")
12826
12908
  .option("--json", "Print a JSON envelope.")
12827
12909
  .action(async (sequence, options) => {
12828
- await handleAsyncAction("sequences stats", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/stats`));
12910
+ await handleSequenceReadAction("sequences stats", options, () => requestOxygen(`/api/cli/sequences/${encodeURIComponent(sequence)}/stats`), formatSequenceStatsHealth);
12829
12911
  }))
12830
12912
  .addCommand(new Command("esp")
12831
12913
  .description("ESP breakdown of this sequence's email sending scope: which provider each enrolled lead's mail routes through (google/microsoft, plus named gateways like Proofpoint/Mimecast and non-matchable hosts), folded against your sending mailbox pool. Shows how many leads the CURRENT --esp-matching mode would defer before you turn it on. Resolves domains from MX + SPF (0 credits, DNS only) and caches the result, warming the send path. Preview before flipping strict/prefer.")
@@ -21082,6 +21164,185 @@ function renderTextTable(headers, rows) {
21082
21164
  const separator = ` ${widths.map((width) => "-".repeat(width)).join(" ")}`;
21083
21165
  return [renderRow(headers), separator, ...rows.map(renderRow)];
21084
21166
  }
21167
+ // Sequence state is derived only by the shared API. These helpers render those
21168
+ // facts; they never infer flowing/blocked/live client-side. JSON remains the
21169
+ // untouched API data inside the standard CLI envelope.
21170
+ const SEQUENCE_OPERATIONAL_STATE_ORDER = ["empty", "drained", "blocked", "degraded", "flowing", "paced"];
21171
+ async function handleSequenceReadAction(command, options, action, render) {
21172
+ try {
21173
+ const data = await action();
21174
+ if (options.json) {
21175
+ writeJson(success(command, data));
21176
+ return;
21177
+ }
21178
+ process.stdout.write(isRecord(data) ? render(data) : `${JSON.stringify(data, null, 2)}\n`);
21179
+ }
21180
+ catch (error) {
21181
+ emitCliFailure(command, error);
21182
+ }
21183
+ }
21184
+ function formatSequenceListHealth(data) {
21185
+ const sequences = Array.isArray(data.sequences) ? data.sequences.filter(isRecord) : [];
21186
+ const lines = [...formatSequenceFleetSummary(data.fleet), ""];
21187
+ if (sequences.length === 0) {
21188
+ lines.push("No sequences match these filters.");
21189
+ }
21190
+ else {
21191
+ lines.push(...renderTextTable(["SEQUENCE", "PERSISTED", "OPERATIONAL", "OPEN WORK", "SENT / FAILED", "REASONS"], sequences.map((sequence) => sequenceHealthRow(sequence))));
21192
+ lines.push("", ...sequenceInspectionLinks(sequences));
21193
+ }
21194
+ const listLink = stringValue(data.web_url) ?? stringValue(data.deepLink);
21195
+ if (listLink)
21196
+ lines.push(listLink);
21197
+ return `${lines.join("\n")}\n`;
21198
+ }
21199
+ function formatSequenceStatsHealth(data) {
21200
+ const stats = recordValue(data.stats);
21201
+ const operational = recordValue(stats.operational);
21202
+ const lines = [
21203
+ stringValue(data.name) ?? stringValue(data.sequence_id) ?? "Sequence",
21204
+ `Persisted status: ${stringValue(data.status) ?? "unknown"}`,
21205
+ `Operational state: ${stringValue(operational.operationalState) ?? "unknown (legacy response)"}`,
21206
+ `Open work: ${formatSequenceOpenWork(operational.openWork)}`,
21207
+ `Reasons: ${formatSequenceReasons(operational)}`,
21208
+ `${formatFailureWindow(operational.failureAnalytics)} outcomes: ${formatSequenceFailureTotals(operational.failureAnalytics)}`,
21209
+ ];
21210
+ const failureMix = formatCountRecord(recordValue(recordValue(operational.failureAnalytics).byFailureClass));
21211
+ if (failureMix !== "—")
21212
+ lines.push(`Failure mix: ${failureMix}`);
21213
+ lines.push(...formatActionKindBreakdown(operational.failureAnalytics));
21214
+ lines.push("", ...renderTextTable(["FUNNEL", "COUNT"], [
21215
+ ["Enrolled", numberText(stats.enrolled)],
21216
+ ["Invites sent", numberText(stats.invitesSent ?? stats.connectionRequestsSent)],
21217
+ ["Connected", numberText(stats.connected ?? stats.connectionRequestsAccepted)],
21218
+ ["Messages sent", numberText(stats.messagesSent)],
21219
+ ["Replies", numberText(stats.totalReplies ?? stats.replied)],
21220
+ ]));
21221
+ const link = stringValue(data.web_url) ?? stringValue(data.deepLink);
21222
+ if (link)
21223
+ lines.push("", link);
21224
+ return `${lines.join("\n")}\n`;
21225
+ }
21226
+ function formatSequenceAnalyticsHealth(data) {
21227
+ const analytics = recordValue(data.analytics);
21228
+ const summary = recordValue(analytics.operationalSummary);
21229
+ const failureAnalytics = summary.failureAnalytics;
21230
+ const sequences = Array.isArray(analytics.sequences) ? analytics.sequences.filter(isRecord) : [];
21231
+ const range = recordValue(analytics.range);
21232
+ const from = stringValue(range.from);
21233
+ const to = stringValue(range.to);
21234
+ const rangeLabel = from || to ? `${from ?? "start"} → ${to ?? "now"}` : stringValue(range.preset) ?? "selected window";
21235
+ const lines = [
21236
+ `Sequencer analytics (${rangeLabel})`,
21237
+ ...formatSequenceFleetSummary(analytics.fleet),
21238
+ `Open work: ${formatSequenceOpenWork(summary.openWork)}`,
21239
+ `Selected-window outcomes: ${formatSequenceFailureTotals(failureAnalytics)}`,
21240
+ `Failure mix: ${formatCountRecord(recordValue(recordValue(failureAnalytics).byFailureClass))}`,
21241
+ ...formatActionKindBreakdown(failureAnalytics),
21242
+ "",
21243
+ ];
21244
+ if (sequences.length === 0) {
21245
+ lines.push("No sequences in this analytics scope.");
21246
+ }
21247
+ else {
21248
+ lines.push(...renderTextTable(["SEQUENCE", "PERSISTED", "OPERATIONAL", "OPEN WORK", "SENT / FAILED", "REASONS"], sequences.map((sequence) => sequenceHealthRow(sequence))));
21249
+ lines.push("", ...sequenceInspectionLinks(sequences));
21250
+ }
21251
+ const link = stringValue(data.web_url) ?? stringValue(data.deepLink);
21252
+ if (link)
21253
+ lines.push(link);
21254
+ return `${lines.join("\n")}\n`;
21255
+ }
21256
+ function sequenceHealthRow(sequence) {
21257
+ const operational = recordValue(sequence.operational);
21258
+ return [
21259
+ stringValue(sequence.name) ?? stringValue(sequence.slug) ?? "—",
21260
+ stringValue(sequence.status) ?? "unknown",
21261
+ stringValue(operational.operationalState) ?? "unknown",
21262
+ formatSequenceOpenWork(operational.openWork),
21263
+ formatSequenceFailureTotals(operational.failureAnalytics),
21264
+ formatSequenceReasons(operational),
21265
+ ];
21266
+ }
21267
+ function sequenceInspectionLinks(sequences) {
21268
+ return sequences.flatMap((sequence) => {
21269
+ const exactLink = stringValue(sequence.web_url) ?? stringValue(sequence.deepLink);
21270
+ return exactLink ? [`Inspect ${stringValue(sequence.name) ?? stringValue(sequence.slug) ?? "Sequence"}: ${exactLink}`] : [];
21271
+ });
21272
+ }
21273
+ function formatSequenceFleetSummary(value) {
21274
+ const fleet = recordValue(value);
21275
+ if (Object.keys(fleet).length === 0)
21276
+ return ["Fleet counts: unavailable (legacy API response)"];
21277
+ const states = recordValue(fleet.byOperationalState);
21278
+ return [
21279
+ `Fleet: ${numberText(fleet.markedActive)} marked active · ${numberText(fleet.operationallyLive)} operationally live · ${numberText(fleet.total)} total`,
21280
+ `States: ${SEQUENCE_OPERATIONAL_STATE_ORDER.map((state) => `${state} ${numberText(states[state])}`).join(" · ")}`,
21281
+ ];
21282
+ }
21283
+ function formatSequenceOpenWork(value) {
21284
+ const work = recordValue(value);
21285
+ if (Object.keys(work).length === 0)
21286
+ return "unknown";
21287
+ return [
21288
+ `enr ${numberText(work.enrollments)}`,
21289
+ `act ${numberText(work.actions)}`,
21290
+ `now ${numberText(work.actionableNow)}`,
21291
+ `flight ${numberText(work.inFlight)}`,
21292
+ `paced ${numberText(work.paced)}`,
21293
+ `blocked ${numberText(work.blocked)}`,
21294
+ `unresolved ${numberText(work.unresolvedFailures)}`,
21295
+ ].join(" · ");
21296
+ }
21297
+ function formatSequenceFailureTotals(value) {
21298
+ const analytics = recordValue(value);
21299
+ if (Object.keys(analytics).length === 0)
21300
+ return "unknown";
21301
+ return `sent ${numberText(analytics.sent)} · failed ${numberText(analytics.terminalFailed)} · deferred ${numberText(analytics.deferrals)}`;
21302
+ }
21303
+ function formatFailureWindow(value) {
21304
+ const windowValue = recordValue(recordValue(value).window);
21305
+ const from = stringValue(windowValue.from);
21306
+ const to = stringValue(windowValue.to);
21307
+ return from || to ? `Selected-window ${from ?? "start"} → ${to ?? "now"}` : "Lifetime";
21308
+ }
21309
+ function formatSequenceReasons(operational) {
21310
+ const reasonCodes = Array.isArray(operational.reasonCodes)
21311
+ ? operational.reasonCodes.filter((reason) => typeof reason === "string")
21312
+ : [];
21313
+ if (reasonCodes.length > 0)
21314
+ return reasonCodes.join(", ");
21315
+ return stringValue(operational.operationalState) ? "none reported" : "unknown";
21316
+ }
21317
+ function formatActionKindBreakdown(value) {
21318
+ const byActionKind = recordValue(recordValue(value).byActionKind);
21319
+ const entries = Object.entries(byActionKind).filter((entry) => isRecord(entry[1])).sort(([left], [right]) => left.localeCompare(right));
21320
+ if (entries.length === 0)
21321
+ return [];
21322
+ return ["By action kind:", ...entries.map(([kind, bucket]) => {
21323
+ const mix = formatCountRecord(recordValue(bucket.byFailureClass));
21324
+ return ` ${kind}: ${formatSequenceFailureTotals(bucket)}${mix === "—" ? "" : ` · failures ${mix}`}`;
21325
+ })];
21326
+ }
21327
+ function formatCountRecord(value) {
21328
+ const entries = Object.entries(value)
21329
+ .map(([key, count]) => [key, numericValue(count)])
21330
+ .filter((entry) => entry[1] !== null && entry[1] > 0)
21331
+ .sort((left, right) => right[1] - left[1] || left[0].localeCompare(right[0]));
21332
+ return entries.length > 0 ? entries.map(([key, count]) => `${key} ${count}`).join(" · ") : "—";
21333
+ }
21334
+ function recordValue(value) {
21335
+ return isRecord(value) ? value : {};
21336
+ }
21337
+ function stringValue(value) {
21338
+ return typeof value === "string" && value.length > 0 ? value : null;
21339
+ }
21340
+ function numericValue(value) {
21341
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
21342
+ }
21343
+ function numberText(value) {
21344
+ return String(numericValue(value) ?? 0);
21345
+ }
21085
21346
  function formatSequenceVariants(data) {
21086
21347
  const styles = ansi(output.isTTY === true && !process.env.NO_COLOR);
21087
21348
  const byVariant = Array.isArray(data.by_variant) ? data.by_variant : [];
package/dist/skills.js CHANGED
@@ -208,6 +208,13 @@ options, runtime = {}) {
208
208
  const env = runtime.env ?? process.env;
209
209
  const platform = runtime.platform ?? process.platform;
210
210
  const spawn = runtime.spawnSync ?? wrapExecFileSync(runtime.execFileSync) ?? spawnSync;
211
+ const installLocations = resolveSkillInstallLocations(agents, options.project === true, env);
212
+ // The vendored installer writes immediately once spawned. Tell humans and
213
+ // agents the exact destination first so a default global install is never a
214
+ // surprise, while keeping stdout as one stable JSON envelope under --json.
215
+ process.stderr.write(`note: installing Oxygen skills (${options.project ? "project" : "global"}) into ${installLocations
216
+ .map((location) => `${location.agent}=${location.path}`)
217
+ .join(", ")}\n`);
211
218
  // A specific shortcut skill can declare prerequisite skills that its SKILL.md
212
219
  // invokes (e.g. oxygen-clay-migration → oxygen-gtm, whose recipes it lives in).
213
220
  // Installing it alone would leave those missing, so pull them in too. Best-
@@ -304,6 +311,7 @@ options, runtime = {}) {
304
311
  skill,
305
312
  ...(dependencySkills.length > 0 ? { dependency_skills: dependencySkills } : {}),
306
313
  scope: options.project ? "project" : "global",
314
+ install_locations: installLocations,
307
315
  output: outputs.join(""),
308
316
  // Truthful counts a caller (human or scripting agent) can trust at a
309
317
  // glance: a not-detected agent is never folded into `failed`.
@@ -316,6 +324,23 @@ options, runtime = {}) {
316
324
  summary: summarizeInstallOutcome(agents.length, skippedAgents.length, 0),
317
325
  };
318
326
  }
327
+ function resolveSkillInstallLocations(agents, project, env) {
328
+ const projectRoots = {
329
+ codex: resolve(".agents", "skills"),
330
+ "claude-code": resolve(".claude", "skills"),
331
+ cursor: resolve(".agents", "skills"),
332
+ };
333
+ const globalRoots = {
334
+ codex: join(env.CODEX_HOME?.trim() || join(homedir(), ".codex"), "skills"),
335
+ "claude-code": join(env.CLAUDE_CONFIG_DIR?.trim() || join(homedir(), ".claude"), "skills"),
336
+ cursor: join(homedir(), ".cursor", "skills"),
337
+ };
338
+ const roots = project ? projectRoots : globalRoots;
339
+ return agents.map((agent) => ({
340
+ agent,
341
+ path: roots[agent] ?? (project ? resolve(".agents", "skills") : join(homedir(), ".agents", "skills")),
342
+ }));
343
+ }
319
344
  /**
320
345
  * Stages the requested skill(s) into `dir` so the installer receives a local
321
346
  * filesystem path instead of the org-bound tokenized access URL. Keeping the URL
@@ -25,7 +25,7 @@ export type PricingPlanDefinition = {
25
25
  features: readonly string[];
26
26
  };
27
27
  export type PricingPlanMetadata = Record<string, unknown> | null | undefined;
28
- export declare const CONTACT_SALES_URL = "mailto:support@oxygen-agent.com";
28
+ export declare const CONTACT_SALES_URL = "mailto:priority@oxygen-agent.com";
29
29
  export declare const CREDITS_PER_USD = 1000;
30
30
  export declare const TOPUP_USD_CENTS_PER_1K_CREDITS = 125;
31
31
  export declare const CREDIT_TOPUP_MIN_CREDITS = 8000;
@@ -222,7 +222,7 @@ export declare const BASE_PRICING_PLANS: {
222
222
  readonly automationOverageEnabledDefault: false;
223
223
  readonly description: "For high-volume GTM teams that need custom limits and rollout support.";
224
224
  readonly ctaLabel: "Contact sales";
225
- readonly contactHref: "mailto:support@oxygen-agent.com";
225
+ readonly contactHref: "mailto:priority@oxygen-agent.com";
226
226
  readonly features: readonly ["10,000,000+ managed credits / month", "Workflows", "Phone number enrichment on managed credits", "Custom provider routing and BYOK policies", "Dedicated onboarding"];
227
227
  readonly monthlyPriceCents: number | null;
228
228
  readonly monthlyCredits: number | null;
@@ -238,7 +238,7 @@ export declare const BASE_PRICING_PLANS: {
238
238
  readonly automationOverageEnabledDefault: false;
239
239
  readonly description: "Custom procurement, invoicing, limits, and rollout support.";
240
240
  readonly ctaLabel: "Talk to us";
241
- readonly contactHref: "mailto:support@oxygen-agent.com";
241
+ readonly contactHref: "mailto:priority@oxygen-agent.com";
242
242
  readonly features: readonly ["Everything in Team", "Custom usage limits", "Volume pricing", "Dedicated support"];
243
243
  readonly monthlyPriceCents: number | null;
244
244
  readonly monthlyCredits: number | null;
@@ -4,11 +4,11 @@ export const WEEKLY_USAGE_WINDOW_DAYS = 7;
4
4
  export const BILLING_CURRENCIES = ["usd"];
5
5
  export const DEFAULT_BILLING_CURRENCY = "usd";
6
6
  // Scale/Enterprise contact routes to the shared support inbox, not to a
7
- // founder's calendar. support@oxygen-agent.com is the Plain Email channel
7
+ // founder's calendar. priority@oxygen-agent.com is the Plain Email channel
8
8
  // (PLAIN_SUPPORT_EMAIL_ADDRESS), so an enterprise enquiry lands in the support
9
9
  // queue as a Thread that either founder can pick up, instead of hard-coding one
10
10
  // person's booking page into every public pricing card.
11
- export const CONTACT_SALES_URL = "mailto:support@oxygen-agent.com";
11
+ export const CONTACT_SALES_URL = "mailto:priority@oxygen-agent.com";
12
12
  // One Oxygen credit is worth $0.001 of managed work: $1 = 1,000 credits.
13
13
  // Subscription plans convert at face value — every subscription dollar grants
14
14
  // exactly 1,000 credits — while on-demand top-up packs price the same credit
@@ -69,12 +69,12 @@ export const OXYGEN_CAPABILITY_ROUTES = [
69
69
  id: "support-and-feedback",
70
70
  layer: "Control",
71
71
  primitive: null,
72
- owns: "Customer support tickets, product feedback, transcript-backed reports, and ticket follow-up.",
72
+ owns: "Customer support conversations, structured tickets, product feedback, transcript-backed reports, and Thread follow-up.",
73
73
  notFor: "Primitive run diagnosis when no support report is needed; inspect Observability first.",
74
- execution: "File or inspect a hosted support record and return its deep link; never send a customer reply automatically.",
74
+ execution: "Start or continue a hosted support conversation, optionally file structured metadata, and return its deep link; never send a customer reply automatically.",
75
75
  posture: "workspace_write",
76
76
  gatewayTools: ["oxygen_feedback", "oxygen_support_ticket_create", "oxygen_support_tickets_list"],
77
- gatewayCommands: ["feedback", "support file", "support list"],
77
+ gatewayCommands: ["feedback", "support chat", "support file", "support list"],
78
78
  skills: ["oxygen-feedback"],
79
79
  endpointSections: ["feedback", "support"],
80
80
  intentTerms: ["support", "feedback", "bug report", "feature request", "report issue", "ticket", "complaint"],
@@ -61,6 +61,11 @@ export type FeatureGateResolution = {
61
61
  * - OXYGEN_PUBLISHING_ENABLED — (b). Fail-closed production switch for posting
62
62
  * to real provider accounts (`apps/web/src/app/(app)/(dashboard)/publishing/
63
63
  * data.ts`). Wrong "on" = public posts from customer accounts.
64
+ * - OXYGEN_WORKER_AUTOSCALE — (b). Arms the worker fleet autoscaler, which STOPS
65
+ * production Machines (`apps/worker/src/fleet-autoscaler.ts`). Its worst wrong
66
+ * value takes the fleet down while work is queued, so it must never depend on a
67
+ * flag service being reachable — and "the flag service is unreachable" is exactly
68
+ * the kind of incident during which you least want the fleet to move.
64
69
  * - OXYGEN_AGENTS_ENABLED — (b). Authorizes autonomous Agent run execution —
65
70
  * tool authority AND credit spend (`apps/web/src/lib/agent-runtime.ts`,
66
71
  * `apps/worker/src/agent-cycle.ts`). Same class as publishing.
@@ -52,6 +52,11 @@
52
52
  * - OXYGEN_PUBLISHING_ENABLED — (b). Fail-closed production switch for posting
53
53
  * to real provider accounts (`apps/web/src/app/(app)/(dashboard)/publishing/
54
54
  * data.ts`). Wrong "on" = public posts from customer accounts.
55
+ * - OXYGEN_WORKER_AUTOSCALE — (b). Arms the worker fleet autoscaler, which STOPS
56
+ * production Machines (`apps/worker/src/fleet-autoscaler.ts`). Its worst wrong
57
+ * value takes the fleet down while work is queued, so it must never depend on a
58
+ * flag service being reachable — and "the flag service is unreachable" is exactly
59
+ * the kind of incident during which you least want the fleet to move.
55
60
  * - OXYGEN_AGENTS_ENABLED — (b). Authorizes autonomous Agent run execution —
56
61
  * tool authority AND credit spend (`apps/web/src/lib/agent-runtime.ts`,
57
62
  * `apps/worker/src/agent-cycle.ts`). Same class as publishing.
@@ -60,6 +65,7 @@
60
65
  * `findFeatureGateRegistryViolations` fails the build's tests if one ever is.
61
66
  */
62
67
  export const NEVER_FLAGGABLE = [
68
+ "OXYGEN_WORKER_AUTOSCALE",
63
69
  "OXYGEN_PUBLISHING_ENABLED",
64
70
  "OXYGEN_AGENTS_ENABLED",
65
71
  "OXYGEN_TELEMETRY_ENABLED",
@@ -35,6 +35,7 @@ export * from "./feature-gates.js";
35
35
  export * from "./hosted-ai.js";
36
36
  export * from "./identifiers.js";
37
37
  export * from "./knowledge-constants.js";
38
+ export * from "./knowledge-bootstrap.js";
38
39
  export * from "./knowledge-links.js";
39
40
  export * from "./knowledge-markdown.js";
40
41
  export * from "./knowledge-seed-content.js";
@@ -35,6 +35,7 @@ export * from "./feature-gates.js";
35
35
  export * from "./hosted-ai.js";
36
36
  export * from "./identifiers.js";
37
37
  export * from "./knowledge-constants.js";
38
+ export * from "./knowledge-bootstrap.js";
38
39
  export * from "./knowledge-links.js";
39
40
  export * from "./knowledge-markdown.js";
40
41
  export * from "./knowledge-seed-content.js";