@oxygen-agent/cli 1.1014.19 → 1.1014.69

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 (31) hide show
  1. package/README.md +1 -1
  2. package/dist/command-manifest.js +1 -1
  3. package/dist/help.js +21 -1
  4. package/dist/index.js +50 -14
  5. package/dist/json-output.d.ts +3 -0
  6. package/dist/json-output.js +33 -0
  7. package/dist/workspace-skill-commands.d.ts +9 -0
  8. package/dist/workspace-skill-commands.js +134 -0
  9. package/node_modules/@oxygen/shared/dist/capability-discovery.js +9 -0
  10. package/node_modules/@oxygen/shared/dist/contracts/operations.js +4 -3
  11. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.d.ts +1 -1
  12. package/node_modules/@oxygen/shared/dist/contracts/table-query-grid.js +7 -4
  13. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.d.ts +14 -0
  14. package/node_modules/@oxygen/shared/dist/contracts/table-query-raw.js +18 -0
  15. package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.d.ts +6 -0
  16. package/node_modules/@oxygen/shared/dist/contracts/workspace-skills.js +62 -0
  17. package/node_modules/@oxygen/shared/dist/diagnostic-storage-lock.js +5 -1
  18. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +178 -63
  19. package/node_modules/@oxygen/shared/dist/email-health-status.d.ts +11 -0
  20. package/node_modules/@oxygen/shared/dist/email-health-status.js +16 -0
  21. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +3 -2
  22. package/node_modules/@oxygen/shared/dist/hosted-ai.js +9 -6
  23. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +4 -2
  24. package/node_modules/@oxygen/shared/dist/langfuse.js +123 -66
  25. package/node_modules/@oxygen/shared/dist/table-run-summary.d.ts +2 -0
  26. package/node_modules/@oxygen/shared/dist/table-run-summary.js +26 -0
  27. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  28. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  29. package/node_modules/@oxygen/shared/dist/workspace-skills.d.ts +26 -0
  30. package/node_modules/@oxygen/shared/dist/workspace-skills.js +102 -0
  31. 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.1014.19
37
+ Version: 1.1014.69
@@ -306,7 +306,7 @@ export function searchCommandManifest(manifest, query, requestedLimit = DEFAULT_
306
306
  commands: matches.slice(0, limit).map(({ command, score }) => ({
307
307
  name: command.name,
308
308
  group: command.group,
309
- description: command.description,
309
+ description: (command.description.split(/(?<=[.!?])\s/)[0] ?? command.description).slice(0, 180),
310
310
  spends_credits: command.spends_credits,
311
311
  mutates: command.mutates,
312
312
  destructive: command.destructive,
package/dist/help.js CHANGED
@@ -126,6 +126,17 @@ const HELP_GROUPS = [
126
126
  ];
127
127
  const FALLBACK_HEADING = "Other commands:";
128
128
  export function applyOxygenHelp(program, binaryName) {
129
+ program.option("--help-full", "Show expanded command and option descriptions.");
130
+ const configure = (command) => {
131
+ command.configureHelp({
132
+ commandDescription: (cmd) => shortHelpDescription(cmd.description()),
133
+ subcommandDescription: (cmd) => shortHelpDescription(cmd.description()),
134
+ optionDescription: (option) => shortHelpDescription(option.description),
135
+ });
136
+ for (const child of command.commands)
137
+ configure(child);
138
+ };
139
+ configure(program);
129
140
  const headingByCommand = new Map();
130
141
  for (const group of HELP_GROUPS) {
131
142
  for (const name of group.commands)
@@ -140,6 +151,7 @@ export function applyOxygenHelp(program, binaryName) {
140
151
  program.addHelpText("before", [
141
152
  "",
142
153
  "Agent quickstart:",
154
+ " Help is concise; --help-full or commands get <name> --json retains complete grammar.",
143
155
  ` 1. ${binaryName} login connect this terminal (or set OXYGEN_API_KEY)`,
144
156
  ` ${binaryName} whoami --summary-json confirm the active workspace and identity (0 credits)`,
145
157
  // `login` auto-installs the skills, but an agent authenticating with
@@ -171,6 +183,12 @@ export function applyOxygenHelp(program, binaryName) {
171
183
  ].join("\n"));
172
184
  program.addHelpText("afterAll", `\nExit codes: ${exitCodeLine}`);
173
185
  }
186
+ function shortHelpDescription(description) {
187
+ if (process.argv.includes("--help-full"))
188
+ return description;
189
+ const first = description.split(/(?<=[.!?])\s/)[0] ?? description;
190
+ return first.length <= 180 ? first : `${first.slice(0, 177)}…`;
191
+ }
174
192
  /**
175
193
  * Turn commander's own edit-distance suggester on for EVERY command.
176
194
  *
@@ -443,7 +461,9 @@ function buildUnknownCommandHint(parent, path, token) {
443
461
  // command.
444
462
  const prefixMatches = names.filter((name) => name.startsWith(lower));
445
463
  const suggestion = synonym ?? (prefixMatches.length === 1 ? prefixMatches[0] : null);
446
- const helpLine = `Run \`${path} --help\` for the ${names.length} subcommands of \`${path}\`.`;
464
+ const query = path.split(" ").slice(1).concat(token).join(" ");
465
+ const quotedQuery = `'${query.replace(/'/g, "'\\''")}'`;
466
+ const helpLine = `Find the command with \`${path.split(" ")[0]} commands search ${quotedQuery} --json\`; hydrate the selected name with \`${path.split(" ")[0]} commands get <name> --json\`.`;
447
467
  return suggestion
448
468
  ? `Hint: \`${path}\` has no \`${token}\`. Did you mean \`${path} ${suggestion}\`? ${helpLine}`
449
469
  : `Hint: \`${path}\` has no \`${token}\`. ${helpLine}`;
package/dist/index.js CHANGED
@@ -20,6 +20,7 @@ import { applyOxygenHelp, argvRequestsJson, commanderUsageFailure, commandNameFo
20
20
  import { homeStandupMarksSeen } from "./home-standup.js";
21
21
  import { ensureFreshCliForApiUrl, requestOxygen } from "./http-client.js";
22
22
  import { formatInboxNeedsReplyNotice } from "./inbox-needs-reply-notice.js";
23
+ import { serializeCliJson, writeExclusiveJson } from "./json-output.js";
23
24
  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";
24
25
  import { registerKnowledgeRepositoryCommands } from "./knowledge-repository-commands.js";
25
26
  import { runLocalCustomHttpColumn } from "./local-custom-http-column.js";
@@ -42,6 +43,7 @@ import { registerVisualCommands } from "./visual-commands.js";
42
43
  import { WHATSAPP_ACCOUNT_FLAGS, whatsappAccountQuery } from "./whatsapp-account-flags.js";
43
44
  import { formatWorkflowPlanLimitNotices } from "./workflow-plan-limit-notices.js";
44
45
  import { WORKFLOW_RUN_LIST_FLAG_OVERLAY, readWorkflowRunListFlags } from "./workflow-run-list-flags.js";
46
+ import { registerWorkspaceSkillCommands } from "./workspace-skill-commands.js";
45
47
  import { addProviderLimitFlags, addSuppressionListFlags, providerLimitBody, providerLimitQuery, suppressionListQuery } from "./workspace-utility-flags.js";
46
48
  import { isRecipeDefinition } from "@oxygen/recipe-sdk";
47
49
  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, formatCopilotPlanDuration, formatCopilotPlanSeconds, formatPublicBudgetScopes, SUBJECT_PATH_FORMS_PROSE, formatSubjectPath, exitCodeForOxygenError, parseSubjectPath, parseSubjectRef, parseWorkflowStatusChange, chunk, isVersionGreater, isVersionLess, KNOWLEDGE_BOOTSTRAP_MAX_CREDITS, MAX_CLI_JSON_BODY_BYTES, MAX_MCP_TOOL_NAME_LENGTH, normalizeCopilotPlanStepStatus, OXYGEN_CAPABILITY_ROUTES, OXYGEN_VERSION, OxygenError, getCapabilityRouteMatch, inferUserCapabilityRoute, parseKnowledgePageMarkdown, PLAN_LIMITS, serializeCapabilityRoute, sleep, success, TAG_KINDS_PROSE, toFailure, workflowMcpToolName, } from "@oxygen/shared";
@@ -65,6 +67,7 @@ import { SEQUENCE_LIST_PROJECTION_CONTRACT } from "@oxygen/shared/contracts/sequ
65
67
  import { TABLE_FEED_UPDATE_CONTRACT, tableFeedUpdateIssues } from "@oxygen/shared/contracts/table-feed-update";
66
68
  import { TABLE_LIST_QUERY_CONTRACT, tableListQueryParams } from "@oxygen/shared/contracts/table-list";
67
69
  import { TABLE_QUERY_GRID_CONTRACT } from "@oxygen/shared/contracts/table-query-grid";
70
+ import { TABLE_QUERY_CLI_DEFAULT_LIMIT, TABLE_QUERY_CLI_DEFAULT_MAX_CHARS, tableQueryRawResultFits, tableQueryRawRecovery } from "@oxygen/shared/contracts/table-query-raw";
68
71
  import { WHATSAPP_ACCOUNTS_READ_CONTRACT } from "@oxygen/shared/contracts/whatsapp-reads";
69
72
  import { WORKFLOW_RUN_LIST_CONTRACT, workflowRunListQueryParams } from "@oxygen/shared/contracts/workflow-run-list";
70
73
  import { COPILOT_ASK_USER_TOOL_NAME, normalizeCopilotQuestions } from "@oxygen/shared/copilot-questions";
@@ -79,6 +82,7 @@ import { TAG_COLORS } from "@oxygen/shared/select-options";
79
82
  import { SEQUENCE_LIFECYCLES, sequenceDisplayStatus } from "@oxygen/shared/sequence-lifecycle";
80
83
  import { lookupColumnDataType } from "@oxygen/shared/table-field-types";
81
84
  import { FILTER_TREE_OPERATORS } from "@oxygen/shared/table-filter-operators";
85
+ import { tableRunSummary } from "@oxygen/shared/table-run-summary";
82
86
  import { assertRecipeBundleSafe, assertPortableWorkflowDefinition, assertWorkflowGraphManifest, assertWorkflowManifest, buildRecipeManifest, compileWorkflowDefinition, isAnyWorkflowManifest, isRecipeManifest, isWorkflowDefinition, isWorkflowGraphManifest, isWorkflowManifest, hashPortableWorkflowGraphManifest, WORKFLOW_GRAPH_COMPILER_VERSION, WORKFLOW_GRAPH_MANIFEST_VERSION, } from "@oxygen/workflows";
83
87
  import { Command, CommanderError, Option } from "commander";
84
88
  import { execFileSync } from "node:child_process";
@@ -435,7 +439,7 @@ const RECIPE_ESBUILD_NODE_PATHS = [
435
439
  resolve(CLI_MODULE_DIR, "../../../node_modules"),
436
440
  ];
437
441
  function writeJson(value) {
438
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
442
+ process.stdout.write(serializeCliJson(value, process.argv.includes("--pretty")));
439
443
  }
440
444
  function linkBar(coverage, width = 36) {
441
445
  const filled = Math.max(0, Math.min(width, Math.round(coverage * width)));
@@ -3668,6 +3672,7 @@ export function createProgram() {
3668
3672
  .name(binaryName)
3669
3673
  .description("Revenue infrastructure for B2B startups — agent-operated GTM: tables, enrichment, sequences, workflows, CRM, knowledge. Everything here also runs in the web app and over MCP; every state-changing action returns a web_url deep-link.")
3670
3674
  .version(OXYGEN_VERSION)
3675
+ .option("--pretty", "Indent JSON output (default: compact JSON).")
3671
3676
  .option("--profile <name>", "Use a stored CLI profile for this command.")
3672
3677
  .option("--org <organization>", "Use an organization id, Clerk org id, or slug for this command.");
3673
3678
  program.hook("preAction", (_root, selected) => {
@@ -7237,10 +7242,11 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
7237
7242
  }))
7238
7243
  .addCommand(addContractFlags(new Command("query"), TABLE_QUERY_GRID_CONTRACT, {
7239
7244
  format: { valueName: "<format>", help: "Grid cell previews/state: default 50, cap 200, optional stats/fill/window. Omit for raw filters, fields, search, sorts or offsets." },
7245
+ max_chars: { help: "Maximum compact JSON envelope characters for raw or grid reads (1,000-100,000). Raw defaults to 12,000." },
7240
7246
  })
7241
7247
  .description("Read raw Table rows or --format grid cell previews/state with grid.nextCursor, in a JSON envelope. Use tables preview --summary-only for fill rates; tables export <table> --format table renders raw rows. Grid supports stats/fill/window options and refuses raw selectors.")
7242
7248
  .argument("<table>", "Table id or slug.")
7243
- .option("--limit <n>", "Raw: default 100, native cap 2000. Grid: default 50, cap 200.")
7249
+ .option("--limit <n>", "Raw: default 20, native cap 2000; --full restores default 100. Grid: default 50, cap 200.")
7244
7250
  .option("--cursor <cursor>", "Pagination cursor returned by a previous query. Walks forward one page at a time; mutually exclusive with --offset.")
7245
7251
  .option("--offset <n>", "Skip this many rows and start there — jump straight to a row without paging to it. Mutually exclusive with --cursor.")
7246
7252
  .option("--fields <columns>", "Comma-separated column keys or ids to include.")
@@ -7250,14 +7256,21 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
7250
7256
  .option("--formula-values <mode>", "Formula filters are refused by default because displayed formulas are evaluated live. Refresh the formula's stored cells with `columns run <table> <column> --force` (0 credits), then pass 'materialized'; filtering uses that stored snapshot and returns a freshness note. A fill-empty field a bundle adds (definition fill: \"empty\", such as Headcount or Job title) is stored, not live: its cell is the value, so 'materialized' filters it exactly.")
7251
7257
  .option("--sort-json <json>", "Ordered sort rules, e.g. '[{\"columnKey\":\"_created_at\",\"direction\":\"desc\"}]'. Earlier rules dominate. Mutually exclusive with --filter-json.")
7252
7258
  .option("--no-system-fields", "Omit _row_id, _created_at, and _updated_at from returned rows (included by default).")
7259
+ .option("--full", "Read legacy full rows without the default character limit.")
7260
+ .option("--output <path>", "Write full JSON for this page to a new local file; print a small receipt.")
7253
7261
  .option("--json", "Print a JSON envelope.")
7254
7262
  .action(async (table, options) => {
7255
- await handleAsyncAction("tables query", options, () => {
7263
+ await handleAsyncAction("tables query", options, async () => {
7256
7264
  const gridArgs = readContractFlags(options, TABLE_QUERY_GRID_CONTRACT);
7257
- if (gridArgs.format !== "grid" && Object.keys(gridArgs).length > 0) {
7265
+ if (gridArgs.format !== "grid" && Object.keys(gridArgs).some((key) => key !== "max_chars")) {
7258
7266
  throw new OxygenError("invalid_request", "Grid options require --format grid.", { exitCode: 1 });
7259
7267
  }
7260
- const limit = readPositiveInt(options.limit);
7268
+ const full = options.full || Boolean(options.output);
7269
+ if (full && options.format === "grid")
7270
+ throw new OxygenError("invalid_request", "--full/--output require raw rows; omit --format grid.", { exitCode: 2 });
7271
+ const limit = readPositiveInt(options.limit) ?? (options.format !== "grid" && !full ? TABLE_QUERY_CLI_DEFAULT_LIMIT : undefined);
7272
+ if (options.format !== "grid" && !full && gridArgs.max_chars === undefined)
7273
+ gridArgs.max_chars = TABLE_QUERY_CLI_DEFAULT_MAX_CHARS;
7261
7274
  const filters = readFilterJsonOption(options.filterJson);
7262
7275
  const filterTree = readJsonObjectOption(options.filterTreeJson);
7263
7276
  const formulaValues = readFormulaValuesOption(options.formulaValues);
@@ -7276,7 +7289,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
7276
7289
  readOption(options.search) || formulaValues || sorts?.length || (offset !== undefined && offset > 0))) {
7277
7290
  throw new OxygenError("invalid_request", '--format grid takes only table, --limit, --cursor and optional --include-stats/--include-fill; omit --format for filters, fields, search, sorts or offsets.', { exitCode: 1 });
7278
7291
  }
7279
- return requestOxygen("/api/cli/tables/query", {
7292
+ const data = await requestOxygen("/api/cli/tables/query", {
7280
7293
  method: "POST",
7281
7294
  body: {
7282
7295
  table,
@@ -7293,6 +7306,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
7293
7306
  ...(options.systemFields === false ? { include_system_fields: false } : {}),
7294
7307
  },
7295
7308
  });
7309
+ // Enforce the window even against an older compatible serving API.
7310
+ if (options.format !== "grid" && !tableQueryRawResultFits(data, typeof gridArgs.max_chars === "number" ? gridArgs.max_chars : undefined)) {
7311
+ const firstRow = Array.isArray(data.rows) ? data.rows[0] : undefined;
7312
+ throw new OxygenError("table_query_result_too_large", "Table rows exceed the requested result window.", {
7313
+ details: { max_chars: gridArgs.max_chars, ...tableQueryRawRecovery(table, typeof firstRow?._row_id === "string" ? firstRow._row_id : undefined) }, exitCode: 2,
7314
+ });
7315
+ }
7316
+ if (!options.output)
7317
+ return data;
7318
+ const output = await writeExclusiveJson(options.output, success("tables query", data), process.argv.includes("--pretty"));
7319
+ return { output, returned_count: data.returned_count ?? data.returnedCount, total_count: data.total_count ?? data.totalCount, hasMore: data.hasMore, nextCursor: data.nextCursor, web_url: data.web_url };
7296
7320
  });
7297
7321
  }))
7298
7322
  .addCommand(new Command("preview")
@@ -10656,11 +10680,17 @@ Examples:
10656
10680
  .addCommand(new Command("functions")
10657
10681
  .description("List every formula function (name, signature, examples), the operator grammar, and the expression_notes that a signature cannot teach (bare column keys, string escapes, no regex flags, read-time evaluation). The same language powers formula columns and --run-condition gates.")
10658
10682
  .option("--category <category>", "Filter by category: logic, string, number, date, url_email, array, json, null_handling, cross_row.")
10683
+ .option("--query <text>", "Find functions by name, signature or description.")
10659
10684
  .option("--json", "Print a JSON envelope.")
10660
10685
  .action(async (options) => {
10661
10686
  await handleAsyncAction("formulas functions", options, () => {
10662
10687
  const category = readOption(options.category);
10663
- const query = category ? `?category=${encodeURIComponent(category)}` : "";
10688
+ const params = new URLSearchParams();
10689
+ if (category)
10690
+ params.set("category", category);
10691
+ if (readOption(options.query))
10692
+ params.set("query", readOption(options.query));
10693
+ const query = params.size ? `?${params.toString()}` : "";
10664
10694
  return requestOxygen(`/api/cli/tables/formulas/functions${query}`, { method: "GET" });
10665
10695
  });
10666
10696
  }))
@@ -10739,9 +10769,11 @@ Examples:
10739
10769
  .addCommand(new Command("get")
10740
10770
  .description("Get one durable table action run. For a Function run, selection.rowIds are rows of the Function's execution table, not the calling table; list the calling table's rows it wrote with `tables query <caller> --filter-tree-json` on the Function column's \"action_run_id\" path.")
10741
10771
  .argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
10772
+ .option("--full", "Read the full run, including stored inputs and metadata.")
10742
10773
  .option("--json", "Print a JSON envelope.")
10743
10774
  .action(async (runId, options) => {
10744
- await handleAsyncAction("table-runs get", options, () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}`));
10775
+ await handleAsyncAction("table-runs get", options, () => requestOxygen(`/api/cli/table-action-runs/${encodeURIComponent(runId)}?view=${options.full ? "full" : "summary"}`)
10776
+ .then((run) => options.full ? run : tableRunSummary(run)));
10745
10777
  }))
10746
10778
  .addCommand(new Command("items")
10747
10779
  .description("List row items for a durable table action run. For a Function run, list the calling table's rows it wrote with `tables query <caller> --filter-tree-json` on the Function column's \"action_run_id\" path instead of joining items.")
@@ -10766,6 +10798,7 @@ Examples:
10766
10798
  .argument("<run_id>", "Table action run UUID or parent workspace run UUID.")
10767
10799
  .option("--timeout-seconds <n>", "Maximum time to wait. Defaults to 600.")
10768
10800
  .option("--interval-seconds <n>", "Polling interval. Defaults to 5.")
10801
+ .option("--full", "Return the full completed run instead of a progress summary.")
10769
10802
  .option("--json", "Print a JSON envelope.")
10770
10803
  .action(async (runId, options) => {
10771
10804
  await handleAsyncAction("table-runs wait", options, () => waitForTableActionRun(runId, options));
@@ -16689,10 +16722,11 @@ Examples:
16689
16722
  .option("--status <status>", "Filter by enrollment status (pending, active, waiting_connection, waiting_signal, replied, completed, stopped, failed, excluded). pending = enrolled, not started (next_due not_started); waiting_connection = invite sent, not accepted yet. Status is the lead's progress, not its invite: use --invite accepted for the leads that accepted.")
16690
16723
  .option("--step <n>", "Only leads currently on this step (0-based index, as in the sequence's steps).")
16691
16724
  .option("--invite <state>", "Only leads in this LinkedIn invite state: accepted (invited and connected), pending (invited, not accepted yet) or not_invited (no invite: already connected before, suppressed or email-only).")
16692
- .option("--view <view>", "full (default, every enrollment field) or compact (one short row per lead with next_due in words and local time).")
16725
+ .option("--view <view>", "compact (default) or full (every enrollment field) (one short row per lead with next_due in words and local time).")
16693
16726
  .option("--order <order>", "created (default) or due: soonest next_due first, then waiting leads by gives_up_at, then leads with nothing due.")
16694
16727
  .option("--limit <n>", "Maximum enrollments to return (1-500).")
16695
16728
  .option("--offset <n>", "Skip this many enrollments; pass next_offset from the previous page to continue.")
16729
+ .option("--full", "Read every enrollment field (same as --view full).")
16696
16730
  .option("--json", "Print a JSON envelope.")
16697
16731
  .action(async (sequence, options) => {
16698
16732
  await handleAsyncAction("sequences enrollments", options, () => {
@@ -16706,7 +16740,7 @@ Examples:
16706
16740
  const invite = readOption(options.invite);
16707
16741
  if (invite)
16708
16742
  params.set("invite", invite);
16709
- const view = readOption(options.view);
16743
+ const view = options.full ? "full" : readOption(options.view) ?? "compact";
16710
16744
  if (view)
16711
16745
  params.set("view", view);
16712
16746
  const order = readOption(options.order);
@@ -20456,7 +20490,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
20456
20490
  })));
20457
20491
  program
20458
20492
  .command("skills")
20459
- .description("Agent skill discovery and installation commands.")
20493
+ .description("Find and install Oxygen guides, or create and import private workspace skills with skills workspace.")
20460
20494
  .addCommand(new Command("list")
20461
20495
  .description("List Oxygen agent skills available from the hosted skill index.")
20462
20496
  .option("--api-url <url>", "Oxygen app URL. Defaults to OXYGEN_API_URL or https://oxygen-agent.com.")
@@ -20508,6 +20542,7 @@ Full trigger schema: oxygen workflows schema --subject trigger --json
20508
20542
  registerConnectorsCommands(program, handleAsyncAction);
20509
20543
  registerSettingsCommands(program, handleAsyncAction);
20510
20544
  registerCrmSupportFlags(program);
20545
+ registerWorkspaceSkillCommands(program, handleAsyncAction);
20511
20546
  registerCustomerOperationCommands(program, handleAsyncAction);
20512
20547
  registerCrmFileReadCommands(program, handleAsyncAction);
20513
20548
  // After the register* helpers: help groups are assigned to the commands that
@@ -24314,12 +24349,12 @@ function buildGetSurfaceRunWaitConfig(params) {
24314
24349
  defaultTimeoutSeconds: params.defaultTimeoutSeconds,
24315
24350
  defaultIntervalSeconds: params.defaultIntervalSeconds,
24316
24351
  retryTransientNetworkErrors: params.retryTransientNetworkErrors === true,
24317
- fetchRun: (signal) => requestOxygen(`${params.endpoint}/${encodeURIComponent(params.runId)}`, signal ? { signal } : {}),
24352
+ fetchRun: (signal) => requestOxygen(`${params.endpoint}/${encodeURIComponent(params.runId)}${params.view ? `?view=${params.view}` : ""}`, signal ? { signal } : {}),
24318
24353
  isTerminal: params.isTerminal,
24319
24354
  shapeTerminal: (run, status, polls, elapsedMs) => {
24320
24355
  const webUrl = readRecordString(run, "web_url");
24321
24356
  return {
24322
- [params.runKey]: run,
24357
+ [params.runKey]: params.view === "summary" ? tableRunSummary(run) : run,
24323
24358
  [params.runIdKey]: readRecordString(run, "id") ?? params.runId,
24324
24359
  status,
24325
24360
  terminal: true,
@@ -24372,6 +24407,7 @@ function waitForTableActionRun(runId, options) {
24372
24407
  return waitForCliRun(buildGetSurfaceRunWaitConfig({
24373
24408
  runId,
24374
24409
  endpoint: "/api/cli/table-action-runs",
24410
+ view: options.full ? "full" : "summary",
24375
24411
  requestedTimeoutSeconds: options.timeoutSeconds,
24376
24412
  requestedIntervalSeconds: options.intervalSeconds,
24377
24413
  defaultTimeoutSeconds: TABLE_ACTION_RUN_WAIT_DEFAULT_TIMEOUT_SECONDS,
@@ -30623,7 +30659,7 @@ beginCliDiagnostics();
30623
30659
  maybeScheduleBackgroundUpdate();
30624
30660
  const program = createProgram();
30625
30661
  installCommanderExitOverride(program);
30626
- const argv = withoutHelpForUnknownCommand(program, process.argv);
30662
+ const argv = withoutHelpForUnknownCommand(program, process.argv.map((arg) => arg === "--help-full" ? "--help" : arg));
30627
30663
  setCliDiagnosticOperation(commandNameForArgv(program, argv.slice(2)));
30628
30664
  try {
30629
30665
  await program.parseAsync(argv);
@@ -0,0 +1,3 @@
1
+ export declare function serializeCliJson(value: unknown, pretty?: boolean): string;
2
+ /** Own only the newly created file; preserve an existing destination on every failure. */
3
+ export declare function writeExclusiveJson(path: string, value: unknown, pretty?: boolean): Promise<string>;
@@ -0,0 +1,33 @@
1
+ import { open, unlink } from "node:fs/promises";
2
+ import { resolve } from "node:path";
3
+ import { OxygenError } from "@oxygen/shared/cli-result";
4
+ export function serializeCliJson(value, pretty = false) {
5
+ return `${JSON.stringify(value, null, pretty ? 2 : undefined)}\n`;
6
+ }
7
+ /** Own only the newly created file; preserve an existing destination on every failure. */
8
+ export async function writeExclusiveJson(path, value, pretty = false) {
9
+ const destination = resolve(path);
10
+ const serialized = serializeCliJson(value, pretty);
11
+ let handle;
12
+ try {
13
+ handle = await open(destination, "wx", 0o600);
14
+ }
15
+ catch (error) {
16
+ if (error.code === "EEXIST") {
17
+ throw new OxygenError("output_exists", "Output file already exists. Choose a new path.", { exitCode: 2 });
18
+ }
19
+ throw new OxygenError("output_write_failed", "Unable to create the output file. Check the path and permissions.", { exitCode: 2 });
20
+ }
21
+ try {
22
+ await handle.writeFile(serialized, "utf8");
23
+ await handle.close();
24
+ return destination;
25
+ }
26
+ catch {
27
+ await handle.close().catch(() => undefined);
28
+ const removed = await unlink(destination).then(() => true, () => false);
29
+ throw new OxygenError("output_write_failed", removed
30
+ ? "Unable to write the output file. The partial file was removed."
31
+ : "Unable to write or remove the partial output file. Check the destination before retrying.", { exitCode: 2 });
32
+ }
33
+ }
@@ -0,0 +1,9 @@
1
+ import { Command } from "commander";
2
+ import { type WorkspaceSkillFile } from "@oxygen/shared/workspace-skills";
3
+ /** Never follow symlinks or read binary files out of a local skill folder. */
4
+ export declare function readWorkspaceSkillDirectory(directory: string): Promise<WorkspaceSkillFile[]>;
5
+ type Handle = (command: string, options: {
6
+ json?: boolean;
7
+ }, action: () => Promise<unknown>) => Promise<void>;
8
+ export declare function registerWorkspaceSkillCommands(program: Command, handle: Handle): void;
9
+ export {};
@@ -0,0 +1,134 @@
1
+ import { lstat, readdir, readFile } from "node:fs/promises";
2
+ import { join, resolve } from "node:path";
3
+ import { OxygenError } from "@oxygen/shared/cli-result";
4
+ import { authorWorkspaceSkill, parseWorkspaceSkillBundle, WORKSPACE_SKILL_MAX_BYTES, WORKSPACE_SKILL_MAX_FILES, WORKSPACE_SKILL_MAX_FILE_BYTES, } from "@oxygen/shared/workspace-skills";
5
+ import { requestOxygen } from "./http-client.js";
6
+ /** Never follow symlinks or read binary files out of a local skill folder. */
7
+ export async function readWorkspaceSkillDirectory(directory) {
8
+ const root = resolve(directory);
9
+ const files = [];
10
+ let bytes = 0;
11
+ const visit = async (relative, depth) => {
12
+ if (depth > 15)
13
+ throw new OxygenError("invalid_workspace_skill", "Skill directory nesting exceeds 15 levels.", { exitCode: 2 });
14
+ const path = join(root, relative);
15
+ const stat = await lstat(path);
16
+ if (stat.isSymbolicLink())
17
+ throw new OxygenError("invalid_workspace_skill", "Skill imports do not follow symbolic links.", { exitCode: 2 });
18
+ if (stat.isDirectory()) {
19
+ for (const child of (await readdir(path)).sort()) {
20
+ if ([".git", "node_modules", ".DS_Store"].includes(child))
21
+ continue;
22
+ await visit(relative ? `${relative}/${child}` : child, depth + 1);
23
+ }
24
+ return;
25
+ }
26
+ if (!stat.isFile() ||
27
+ stat.size > WORKSPACE_SKILL_MAX_FILE_BYTES ||
28
+ files.length >= WORKSPACE_SKILL_MAX_FILES ||
29
+ bytes + stat.size > WORKSPACE_SKILL_MAX_BYTES) {
30
+ throw new OxygenError("invalid_workspace_skill", "Import supports at most 50 text files, 120 KB per file and 1 MB total.", { exitCode: 2 });
31
+ }
32
+ const buffer = await readFile(path);
33
+ bytes += buffer.byteLength;
34
+ let content;
35
+ try {
36
+ content = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(buffer);
37
+ }
38
+ catch {
39
+ throw new OxygenError("invalid_workspace_skill", `File ${relative} is not UTF-8 text.`, { exitCode: 2 });
40
+ }
41
+ files.push({ path: relative, content });
42
+ };
43
+ await visit("", 0);
44
+ return files;
45
+ }
46
+ export function registerWorkspaceSkillCommands(program, handle) {
47
+ const skills = program.commands.find((command) => command.name() === "skills");
48
+ const workspace = skills
49
+ .command("workspace")
50
+ .description("Create, upload, find and edit private workspace skills in Knowledge.");
51
+ workspace
52
+ .command("create")
53
+ .description("Write a private workspace skill. Stores instructions; 0 credits.")
54
+ .requiredOption("--name <name>", "Lowercase-hyphen skill name.")
55
+ .requiredOption("--description <text>", "When to use this skill.")
56
+ .requiredOption("--instructions <text>", "Instructions to save.")
57
+ .option("--json", "Print a JSON envelope.")
58
+ .action((options, command) => handle("skills workspace create", { json: command.optsWithGlobals().json === true }, async () => {
59
+ authorWorkspaceSkill(options);
60
+ return requestOxygen("/api/cli/skills/workspace/create", {
61
+ method: "POST",
62
+ body: {
63
+ name: options.name,
64
+ description: options.description,
65
+ instructions: options.instructions,
66
+ },
67
+ });
68
+ }));
69
+ workspace
70
+ .command("import")
71
+ .description("Upload SKILL.md or a skill folder from any source into private Knowledge. 0 credits.")
72
+ .option("--file <path>", "Local SKILL.md file.")
73
+ .option("--directory <path>", "Local skill directory including SKILL.md and supporting text files.")
74
+ .option("--url <url>", "Import a public HTTPS SKILL.md URL. Supporting files require --directory or --files-json.")
75
+ .option("--files-json <json>", "Inline array of {path, content}; alternative to --file or --directory.")
76
+ .option("--name <name>", "Destination name; otherwise use SKILL.md frontmatter name.")
77
+ .option("--source-url <url>", "HTTPS source provenance; content must already be downloaded.")
78
+ .option("--json", "Print a JSON envelope.")
79
+ .action((options, command) => handle("skills workspace import", { json: command.optsWithGlobals().json === true }, async () => {
80
+ if ([
81
+ options.file,
82
+ options.directory,
83
+ options.url,
84
+ options.filesJson,
85
+ ].filter((value) => value !== undefined).length !== 1)
86
+ throw new OxygenError("invalid_workspace_skill", "Choose exactly one of --file, --directory, --url or --files-json.", { exitCode: 2 });
87
+ if (options.url !== undefined) {
88
+ if (options.sourceUrl !== undefined)
89
+ throw new OxygenError("invalid_workspace_skill", "--source-url is for uploaded files. A URL import records its own source.", { exitCode: 2 });
90
+ return requestOxygen("/api/cli/skills/workspace/import", {
91
+ method: "POST",
92
+ body: {
93
+ url: options.url,
94
+ ...(options.name ? { name: options.name } : {}),
95
+ },
96
+ });
97
+ }
98
+ let files;
99
+ if (options.directory)
100
+ files = await readWorkspaceSkillDirectory(options.directory);
101
+ else if (options.file) {
102
+ const stat = await lstat(options.file);
103
+ if (!stat.isFile() || stat.size > WORKSPACE_SKILL_MAX_FILE_BYTES)
104
+ throw new OxygenError("invalid_workspace_skill", "Provide a regular SKILL.md text file of at most 120 KB.", { exitCode: 2 });
105
+ files = [
106
+ {
107
+ path: "SKILL.md",
108
+ content: new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(await readFile(options.file)),
109
+ },
110
+ ];
111
+ }
112
+ else {
113
+ try {
114
+ files = JSON.parse(options.filesJson);
115
+ }
116
+ catch {
117
+ throw new OxygenError("invalid_workspace_skill", "--files-json must be a JSON array of {path, content}.", { exitCode: 2 });
118
+ }
119
+ }
120
+ const bundle = parseWorkspaceSkillBundle({
121
+ files,
122
+ ...(options.name ? { name: options.name } : {}),
123
+ ...(options.sourceUrl ? { source_url: options.sourceUrl } : {}),
124
+ });
125
+ return requestOxygen("/api/cli/skills/workspace/import", {
126
+ method: "POST",
127
+ body: {
128
+ name: bundle.name,
129
+ files: bundle.files,
130
+ ...(bundle.source_url ? { source_url: bundle.source_url } : {}),
131
+ },
132
+ });
133
+ }));
134
+ }
@@ -1877,7 +1877,12 @@ function isSubscriptionRetentionIntent(query) {
1877
1877
  return /\b(retention offer|win[ -]?back discount|cancellation (?:reason|feedback)|written cancellation)\b/.test(query)
1878
1878
  || /\b(stay|cancel|cancelling)\b/.test(query) && /\b(discount|next month|subscription reason)\b/.test(query);
1879
1879
  }
1880
+ function isWorkspaceSkillIntent(query) {
1881
+ return /\bskills?\b/.test(query) && /\b(create|author|write|upload|import|edit|update|archive|private|workspace|custom|own|reusable)\b/.test(query);
1882
+ }
1880
1883
  function explicitCapabilityIntent(query) {
1884
+ if (isWorkspaceSkillIntent(query))
1885
+ return ROUTE_BY_ID.get("knowledge-graph") ?? null;
1881
1886
  // Importing an AI answer is GEO evidence, rather than a Table file import.
1882
1887
  if (/\b(?:geo|aeo|ai (?:(?:search|answer) )?visibility|consumer ai answers?|answer engine optimization|generative engine optimization)\b/.test(query))
1883
1888
  return ROUTE_BY_ID.get("geo") ?? null;
@@ -2207,6 +2212,10 @@ function isPublishingPostIntent(query) {
2207
2212
  && !(/\b(find|search|scrape|research)\b/.test(query) && /\b(about|mention|containing)\b/.test(query));
2208
2213
  }
2209
2214
  function recommendationsFor(card, query) {
2215
+ if (card.id === "knowledge-graph" && isWorkspaceSkillIntent(query)) {
2216
+ return { tools: ["oxygen_workspace_skills_list", "oxygen_workspace_skills_create", "oxygen_workspace_skills_import", "oxygen_workspace_skills_get", "oxygen_workspace_skills_update", "oxygen_workspace_skills_archive"],
2217
+ commands: ["skills workspace list", "skills workspace create", "skills workspace import", "skills workspace get", "skills workspace update", "skills workspace archive"] };
2218
+ }
2210
2219
  const contactFind = card.id === "sourcing-and-provider-tools" ? oneShotContactFindingRecommendation(query) : null;
2211
2220
  if (contactFind)
2212
2221
  return contactFind;
@@ -13,6 +13,7 @@ import { TABLE_FEED_UPDATE_OPERATION } from "./table-feed-update.js";
13
13
  import { GEO_OPERATIONS } from "./geo.js";
14
14
  import { CUSTOMER_OPERATIONS } from "./customer-operations.js";
15
15
  import { AGENT_SESSION_OPERATIONS } from "./agent-sessions.js";
16
+ import { WORKSPACE_SKILL_OPERATIONS } from "./workspace-skills.js";
16
17
  /** The route with its `{param}` segments filled in, each URL-encoded. */
17
18
  export function operationPath(spec, params) {
18
19
  return spec.route.replace(/\{(\w+)\}/g, (_match, name) => {
@@ -82,6 +83,6 @@ export function operationFormData(spec, body) {
82
83
  }
83
84
  return form;
84
85
  }
85
- export const OPERATION_SPECS = [SENDER_LIMITS_SET_OPERATION, CONNECTOR_UPDATE_OPERATION, CONNECTOR_EVENTS_REVOKE_OPERATION, WORKSPACE_PAGES_GET_OPERATION, WORKSPACE_PAGES_UPDATE_OPERATION, TABLE_FEED_UPDATE_OPERATION, ...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS];
86
- export const CLI_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS].filter((spec) => spec.generated?.cli !== false);
87
- export const MCP_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS].filter((spec) => spec.generated?.mcp !== false);
86
+ export const OPERATION_SPECS = [SENDER_LIMITS_SET_OPERATION, CONNECTOR_UPDATE_OPERATION, CONNECTOR_EVENTS_REVOKE_OPERATION, WORKSPACE_PAGES_GET_OPERATION, WORKSPACE_PAGES_UPDATE_OPERATION, TABLE_FEED_UPDATE_OPERATION, ...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS, ...WORKSPACE_SKILL_OPERATIONS];
87
+ export const CLI_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS, ...WORKSPACE_SKILL_OPERATIONS].filter((spec) => spec.generated?.cli !== false);
88
+ export const MCP_OPERATIONS = [...CUSTOMER_OPERATIONS, ...GEO_OPERATIONS, ...BROWSER_OPERATIONS, ...AGENT_SESSION_OPERATIONS, ...WORKSPACE_SKILL_OPERATIONS].filter((spec) => spec.generated?.mcp !== false);
@@ -17,7 +17,7 @@ export declare const TABLE_QUERY_GRID_CONTRACT: import("./schema.js").ContractOb
17
17
  }, never>;
18
18
  export declare const TABLE_QUERY_GRID_NATIVE_WINDOW = 10000;
19
19
  export declare const TABLE_QUERY_GRID_BUILTIN_WINDOW = 4000;
20
- export declare const TABLE_QUERY_GRID_COPILOT_WINDOW = 24000;
20
+ export declare const TABLE_QUERY_GRID_COPILOT_WINDOW = 50000;
21
21
  /** The window a built-in host reads tables in; other clients have none. */
22
22
  export declare function tableQueryBuiltinWindow(surface: string | null | undefined): number | undefined;
23
23
  /** Measure the complete native receipt and the escaped built-in host receipt. */
@@ -12,10 +12,13 @@ export const TABLE_QUERY_GRID_NATIVE_WINDOW = 10_000;
12
12
  // Deliberately below Strands' shipped 6000 default; counts the complete host
13
13
  // object rather than only its text. This changes no adapter's outer ceiling.
14
14
  export const TABLE_QUERY_GRID_BUILTIN_WINDOW = 4_000;
15
- // The Copilot hands its model the structured result alone, whole up to 32,000
16
- // characters (ADR 0013, amendment 2026-10-10). One qualified row measured 4,537
17
- // on dev, so the Agent window refused even a single row (eval, 2026-10-11).
18
- export const TABLE_QUERY_GRID_COPILOT_WINDOW = 24_000;
15
+ // The Copilot hands its model the structured result alone, and stores one over
16
+ // 12,000 characters for its session so the model reads only the part it needs
17
+ // (ADR 0013, amendments 2026-10-10 and 2026-10-11). Its window is therefore the
18
+ // largest page the MCP layer returns untrimmed (MAX_STRUCTURED_CONTENT_CHARS in
19
+ // @oxygen/mcp-server), not what fits in a conversation. One qualified row
20
+ // measured 4,537 on dev, so the Agent window refuses even a single row.
21
+ export const TABLE_QUERY_GRID_COPILOT_WINDOW = 50_000;
19
22
  /** The window a built-in host reads tables in; other clients have none. */
20
23
  export function tableQueryBuiltinWindow(surface) {
21
24
  if (surface === "copilot")
@@ -0,0 +1,14 @@
1
+ /** Opt-in at the API boundary; the CLI supplies these defaults. */
2
+ export declare const TABLE_QUERY_RAW_CONTRACT: import("./schema.js").ContractObject<{
3
+ readonly max_chars: {
4
+ type: "integer";
5
+ description?: string;
6
+ minimum?: number;
7
+ maximum?: number;
8
+ "x-clamp"?: true;
9
+ };
10
+ }, never>;
11
+ export declare const TABLE_QUERY_CLI_DEFAULT_LIMIT = 20;
12
+ export declare const TABLE_QUERY_CLI_DEFAULT_MAX_CHARS = 12000;
13
+ export declare function tableQueryRawResultFits(data: Record<string, unknown>, maxChars: number | undefined): boolean;
14
+ export declare function tableQueryRawRecovery(table: string, row?: string): Record<string, unknown>;
@@ -0,0 +1,18 @@
1
+ import { success } from "../cli-result.js";
2
+ import { int, obj } from "./schema.js";
3
+ /** Opt-in at the API boundary; the CLI supplies these defaults. */
4
+ export const TABLE_QUERY_RAW_CONTRACT = obj({
5
+ max_chars: int({ minimum: 1_000, maximum: 100_000, description: "Result envelope chars." }),
6
+ });
7
+ export const TABLE_QUERY_CLI_DEFAULT_LIMIT = 20;
8
+ export const TABLE_QUERY_CLI_DEFAULT_MAX_CHARS = 12_000;
9
+ export function tableQueryRawResultFits(data, maxChars) {
10
+ return maxChars === undefined || JSON.stringify(success("tables query", data)).length <= maxChars;
11
+ }
12
+ export function tableQueryRawRecovery(table, row) {
13
+ return {
14
+ table,
15
+ ...(row ? { row } : {}),
16
+ next_step: "Reduce fields/limit. Inspect a large cell with `tables rows inspect --table <table> --row <row> --field <field> --projection value --path /row/<field>/<key>`; select a JSON Pointer and continue with value_cursor. MCP: hydrate oxygen_table_row_inspect with oxygen_capabilities_schema, then dispatch it through oxygen_capabilities_read. CLI: --full restores legacy reads; --output <path> keeps full JSON outside context.",
17
+ };
18
+ }
@@ -0,0 +1,6 @@
1
+ import type { OperationSpec } from "./operations.js";
2
+ export declare const WORKSPACE_SKILL_FILE_CONTRACT: import("./schema.js").ContractObject<{
3
+ readonly path: import("./schema.js").StringProperty;
4
+ readonly content: import("./schema.js").StringProperty;
5
+ }, "content" | "path">;
6
+ export declare const WORKSPACE_SKILL_OPERATIONS: readonly OperationSpec[];