@oxygen-agent/cli 1.365.3 → 1.377.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/index.js +373 -17
- package/node_modules/@oxygen/shared/dist/billing.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/billing.js +7 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +33 -3
- package/node_modules/@oxygen/shared/dist/index.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/index.js +6 -0
- package/node_modules/@oxygen/shared/dist/langfuse.d.ts +77 -0
- package/node_modules/@oxygen/shared/dist/langfuse.js +231 -0
- package/node_modules/@oxygen/shared/dist/log.js +15 -2
- package/node_modules/@oxygen/shared/dist/tags.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/tags.js +4 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/node_modules/@oxygen/shared/dist/workspace-agents.d.ts +8 -7
- package/node_modules/@oxygen/shared/dist/workspace-agents.js +34 -7
- package/package.json +4 -1
package/README.md
CHANGED
package/dist/index.js
CHANGED
|
@@ -641,6 +641,7 @@ function buildCrmSetupBody(options) {
|
|
|
641
641
|
return {
|
|
642
642
|
objects: readCrmSetupObjects(options.objects),
|
|
643
643
|
mode: resolveLiveDryRunMode(options),
|
|
644
|
+
...(options.withEnrichment === true ? { with_enrichment: true } : {}),
|
|
644
645
|
...(readOption(options.project) ? { project: readOption(options.project) } : {}),
|
|
645
646
|
};
|
|
646
647
|
}
|
|
@@ -2823,6 +2824,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2823
2824
|
.description("Create or repair standard CRM object-backed tables. Defaults to dry-run.")
|
|
2824
2825
|
.option("--objects <objects>", "Comma-separated standard CRM objects to set up. Defaults to companies,people,deals.")
|
|
2825
2826
|
.option("--project <project>", "Project id or slug for created CRM tables.")
|
|
2827
|
+
.option("--with-enrichment", "Also arm the standing cheap-enrichment auto-run on objects that already existed (fresh-created tables arm it by default). New rows then auto-enrich within the per-batch credit cap.")
|
|
2826
2828
|
.option("--dry-run", "Preview CRM setup without creating or repairing tables.")
|
|
2827
2829
|
.option("--live", "Apply CRM setup changes. Default is dry-run.")
|
|
2828
2830
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -2894,7 +2896,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2894
2896
|
}));
|
|
2895
2897
|
}))
|
|
2896
2898
|
.addCommand(new Command("assert")
|
|
2897
|
-
.description("Create or update one CRM record by object identity. Defaults to dry-run.")
|
|
2899
|
+
.description("Create or update one CRM record by object identity. Defaults to dry-run. Note: the table's standing auto-run (auto-enrichment) fires on `tables insert|upsert|import`, not on assert — after asserting new records, run the enrichment columns (`oxygen columns run <table> <column>`) or insert via `oxygen tables insert` when you want them enriched hands-free.")
|
|
2898
2900
|
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
2899
2901
|
.requiredOption("--identity <key=value>", "Identity key/value, for example domain=acme.com or email=ceo@acme.com.")
|
|
2900
2902
|
.option("--values-json <json>", "JSON object of CRM attribute values keyed by column key.")
|
|
@@ -2914,6 +2916,29 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
2914
2916
|
.option("--json", "Print a JSON envelope.")
|
|
2915
2917
|
.action(async (object, rowId, options) => {
|
|
2916
2918
|
await handleAsyncAction("crm get", options, () => requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}`));
|
|
2919
|
+
}))
|
|
2920
|
+
.addCommand(new Command("tag")
|
|
2921
|
+
.description("Add/remove workspace tags on one CRM record (Tags primitive — the same vocabulary as sequences, tables, conversations; see `oxygen tags list`). Delta semantics: --add unions, --remove subtracts, remove wins. Standard objects carry the tags attribute after `oxygen crm setup`.")
|
|
2922
|
+
.argument("<object>", "CRM object slug, such as companies or people.")
|
|
2923
|
+
.argument("<row_id>", "CRM record row id.")
|
|
2924
|
+
.option("--add <tags>", "Comma-separated tags to add.")
|
|
2925
|
+
.option("--remove <tags>", "Comma-separated tags to remove.")
|
|
2926
|
+
.option("--json", "Print a JSON envelope.")
|
|
2927
|
+
.action(async (object, rowId, options) => {
|
|
2928
|
+
await handleAsyncAction("crm tag", options, () => {
|
|
2929
|
+
const add = splitCommaList(options.add);
|
|
2930
|
+
const remove = splitCommaList(options.remove);
|
|
2931
|
+
if (add.length === 0 && remove.length === 0) {
|
|
2932
|
+
throw new OxygenError("invalid_request", "Pass --add and/or --remove.", { exitCode: 1 });
|
|
2933
|
+
}
|
|
2934
|
+
return requestOxygen(`/api/cli/crm/objects/${encodeURIComponent(object)}/records/${encodeURIComponent(rowId)}/tags`, {
|
|
2935
|
+
method: "POST",
|
|
2936
|
+
body: {
|
|
2937
|
+
...(add.length > 0 ? { add } : {}),
|
|
2938
|
+
...(remove.length > 0 ? { remove } : {}),
|
|
2939
|
+
},
|
|
2940
|
+
});
|
|
2941
|
+
});
|
|
2917
2942
|
}))
|
|
2918
2943
|
.addCommand(new Command("relationships")
|
|
2919
2944
|
.description("Define CRM relationships and manage record relationship edges.")
|
|
@@ -3433,11 +3458,20 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
3433
3458
|
.addCommand(new Command("list")
|
|
3434
3459
|
.description("List workspace tables in the current tenant database.")
|
|
3435
3460
|
.option("--project <project>", "Project id or slug to filter by.")
|
|
3461
|
+
.option("--tag <tag>", "Only tables carrying this workspace tag (see `oxygen tags list`).")
|
|
3436
3462
|
.option("--json", "Print a JSON envelope.")
|
|
3437
3463
|
.action(async (options) => {
|
|
3438
|
-
await handleAsyncAction("tables list", options, () =>
|
|
3439
|
-
|
|
3440
|
-
|
|
3464
|
+
await handleAsyncAction("tables list", options, () => {
|
|
3465
|
+
const params = new URLSearchParams();
|
|
3466
|
+
const project = readOption(options.project);
|
|
3467
|
+
if (project)
|
|
3468
|
+
params.set("project", project);
|
|
3469
|
+
const tag = readOption(options.tag);
|
|
3470
|
+
if (tag)
|
|
3471
|
+
params.set("tag", tag);
|
|
3472
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
3473
|
+
return requestOxygen(`/api/cli/tables${qs}`);
|
|
3474
|
+
});
|
|
3441
3475
|
}))
|
|
3442
3476
|
.addCommand(new Command("query")
|
|
3443
3477
|
.description("Query a workspace table by id or slug.")
|
|
@@ -4503,7 +4537,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4503
4537
|
}));
|
|
4504
4538
|
program
|
|
4505
4539
|
.command("tags")
|
|
4506
|
-
.description("Workspace tags: one vocabulary across publishing posts, knowledge pages, sequences, tables, workflows, and
|
|
4540
|
+
.description("Workspace tags: one vocabulary across publishing posts, knowledge pages, sequences, tables, workflows, recipes, and inbox conversations. Tags normalize to lowercase (trimmed, deduped, max 50 per item; any characters). Write tags on the owning surface (e.g. `oxygen inbox tag`, `oxygen tables tag`), then `tags get <tag>` returns every carrier with deep-links.")
|
|
4507
4541
|
.addCommand(new Command("list")
|
|
4508
4542
|
.description("List every workspace tag with per-primitive counts, most-used first.")
|
|
4509
4543
|
.option("--json", "Print a JSON envelope.")
|
|
@@ -4511,11 +4545,23 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4511
4545
|
await handleAsyncAction("tags list", options, () => requestOxygen("/api/cli/tags"));
|
|
4512
4546
|
}))
|
|
4513
4547
|
.addCommand(new Command("get")
|
|
4514
|
-
.description("Everything carrying one tag — publishing posts, knowledge pages, sequences, tables, workflows, and
|
|
4548
|
+
.description("Everything carrying one tag — publishing posts, knowledge pages, sequences, tables, workflows, recipes, and inbox conversations — each with its deep-link.")
|
|
4515
4549
|
.argument("<tag>", "The tag (case-insensitive).")
|
|
4516
4550
|
.option("--json", "Print a JSON envelope.")
|
|
4517
4551
|
.action(async (tag, options) => {
|
|
4518
4552
|
await handleAsyncAction("tags get", options, () => requestOxygen(`/api/cli/tags/${encodeURIComponent(tag)}`));
|
|
4553
|
+
}))
|
|
4554
|
+
.addCommand(new Command("rename")
|
|
4555
|
+
.description("Rename one tag EVERYWHERE it appears (every primitive, archived rows included). Preview-first: without --apply, prints per-kind would-change counts and mutates nothing. Idempotent — safe to re-run after a partial failure.")
|
|
4556
|
+
.argument("<from>", "The current tag (case-insensitive).")
|
|
4557
|
+
.argument("<to>", "The new tag (normalized lowercase).")
|
|
4558
|
+
.option("--apply", "Execute the rename. Without this flag, returns a preview of the counts only.")
|
|
4559
|
+
.option("--json", "Print a JSON envelope.")
|
|
4560
|
+
.action(async (from, to, options) => {
|
|
4561
|
+
await handleAsyncAction("tags rename", options, () => requestOxygen("/api/cli/tags/rename", {
|
|
4562
|
+
method: "POST",
|
|
4563
|
+
body: { from, to, ...(options.apply ? { apply: true } : {}) },
|
|
4564
|
+
}));
|
|
4519
4565
|
}));
|
|
4520
4566
|
program
|
|
4521
4567
|
.command("blueprints")
|
|
@@ -4539,6 +4585,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4539
4585
|
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
4540
4586
|
return requestOxygen(`/api/cli/blueprints${qs}`);
|
|
4541
4587
|
});
|
|
4588
|
+
}))
|
|
4589
|
+
.addCommand(new Command("tag")
|
|
4590
|
+
.description("Replace a saved blueprint's workspace tags (whole set; `--tags \"\"` clears). Works on already-saved blueprints — no re-export needed. See `oxygen tags list` for the vocabulary.")
|
|
4591
|
+
.argument("<blueprint>", "Blueprint slug or id.")
|
|
4592
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
4593
|
+
.option("--json", "Print a JSON envelope.")
|
|
4594
|
+
.action(async (blueprint, options) => {
|
|
4595
|
+
await handleAsyncAction("blueprints tag", options, () => requestOxygen("/api/cli/blueprints/tags", {
|
|
4596
|
+
method: "POST",
|
|
4597
|
+
body: { blueprint, tags: splitCommaList(options.tags) },
|
|
4598
|
+
}));
|
|
4542
4599
|
}))
|
|
4543
4600
|
.addCommand(new Command("describe")
|
|
4544
4601
|
.description("Describe one blueprint (seed or saved) by slug.")
|
|
@@ -4989,7 +5046,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
4989
5046
|
.argument("<table>", "Table id or slug.")
|
|
4990
5047
|
.argument("<column>", "Column id or key.")
|
|
4991
5048
|
.option("--row-id <row_id>", "Workspace row id to run.")
|
|
4992
|
-
.option("--limit <n>", "
|
|
5049
|
+
.option("--limit <n>", "Run the next N rows whose target cell is still empty (--force runs the first N regardless). Repeat until rowCount is 0 to page through a table. Defaults to 10; inline (non-background) hard cap is 25.")
|
|
4993
5050
|
.option("--all", "Run all rows. Requires --background.")
|
|
4994
5051
|
.option("--filter-json <json>", "Row selector filter object or array for background runs. Do not combine with --all, --limit, or --row-id.")
|
|
4995
5052
|
.option("--force", "Run even when the target cell already has a value.")
|
|
@@ -6537,37 +6594,37 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6537
6594
|
// `oxygen knowledge agent`, which configures the AI Knowledge Agent itself.)
|
|
6538
6595
|
program
|
|
6539
6596
|
.command("agent")
|
|
6540
|
-
.description("Agents:
|
|
6597
|
+
.description("Agents: the programmable Workspace Agent plus built-in specialist agents. Publish versioned instructions and policies, start durable runs, trigger them by cron/event/webhook, inspect every event, and control approvals. Web: /agents.")
|
|
6541
6598
|
.addCommand(new Command("list")
|
|
6542
|
-
.description("List the built-in
|
|
6599
|
+
.description("List the Workspace Agent and built-in specialists with state and recent run history.")
|
|
6543
6600
|
.option("--json", "Print a JSON envelope.")
|
|
6544
6601
|
.action(async (options) => {
|
|
6545
6602
|
await handleAsyncAction("agent list", options, () => requestOxygen("/api/cli/agent"));
|
|
6546
6603
|
}))
|
|
6547
6604
|
.addCommand(new Command("get")
|
|
6548
|
-
.description("Show one
|
|
6549
|
-
.argument("<slug>", "
|
|
6605
|
+
.description("Show one agent, including the Workspace Agent's active version and recent runs.")
|
|
6606
|
+
.argument("<slug>", "Agent slug: workspace, inbox-reply-drafts, meeting-notetaker, or knowledge-synthesis.")
|
|
6550
6607
|
.option("--json", "Print a JSON envelope.")
|
|
6551
6608
|
.action(async (slug, options) => {
|
|
6552
6609
|
await handleAsyncAction("agent get", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}`));
|
|
6553
6610
|
}))
|
|
6554
6611
|
.addCommand(new Command("enable")
|
|
6555
|
-
.description("Enable a specialist agent so it resumes its
|
|
6612
|
+
.description("Enable a workspace or specialist agent so it resumes its governed runs.")
|
|
6556
6613
|
.argument("<slug>", "Specialist slug: inbox-reply-drafts, meeting-notetaker, or knowledge-synthesis.")
|
|
6557
6614
|
.option("--json", "Print a JSON envelope.")
|
|
6558
6615
|
.action(async (slug, options) => {
|
|
6559
6616
|
await handleAsyncAction("agent enable", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/enable`, { method: "POST" }));
|
|
6560
6617
|
}))
|
|
6561
6618
|
.addCommand(new Command("disable")
|
|
6562
|
-
.description("Disable a specialist agent so it stops
|
|
6619
|
+
.description("Disable a workspace or specialist agent so it stops new runs on the next worker tick.")
|
|
6563
6620
|
.argument("<slug>", "Specialist slug: inbox-reply-drafts, meeting-notetaker, or knowledge-synthesis.")
|
|
6564
6621
|
.option("--json", "Print a JSON envelope.")
|
|
6565
6622
|
.action(async (slug, options) => {
|
|
6566
6623
|
await handleAsyncAction("agent disable", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/disable`, { method: "POST" }));
|
|
6567
6624
|
}))
|
|
6568
6625
|
.addCommand(new Command("runs")
|
|
6569
|
-
.description("List recent runs across
|
|
6570
|
-
.argument("[slug]", "Optional
|
|
6626
|
+
.description("List recent runs across all agents (or one via <slug>), newest first.")
|
|
6627
|
+
.argument("[slug]", "Optional agent slug to filter to one agent.")
|
|
6571
6628
|
.option("--limit <n>", "Maximum runs to return (1-100). Defaults to 25.")
|
|
6572
6629
|
.option("--json", "Print a JSON envelope.")
|
|
6573
6630
|
.action(async (slug, options) => {
|
|
@@ -6582,6 +6639,203 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6582
6639
|
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
6583
6640
|
return requestOxygen(`/api/cli/agent/runs${suffix}`);
|
|
6584
6641
|
});
|
|
6642
|
+
}))
|
|
6643
|
+
.addCommand(new Command("set")
|
|
6644
|
+
.description("Publish an immutable version (reconfigure) for a runtime agent: the Workspace Agent or a custom agent.")
|
|
6645
|
+
.argument("<slug>", "workspace, or a custom agent slug (from `oxygen agent create`).")
|
|
6646
|
+
.requiredOption("--instructions <text>", "Workspace Agent instructions for this version.")
|
|
6647
|
+
.option("--model-policy-json <json>", "Model policy JSON. Defaults to managed OpenRouter medium.")
|
|
6648
|
+
.option("--tool-policy-json <json>", "Tool policy JSON. Omit restrictions for full eligible catalog access.")
|
|
6649
|
+
.option("--context-policy-json <json>", "Context policy JSON: on_demand or prefetch, optional resolve_args and file_ids.")
|
|
6650
|
+
.option("--skills <csv>", "Comma-separated product skill slugs snapshotted into this immutable version.")
|
|
6651
|
+
.option("--sandbox-policy-json <json>", "Sandbox policy JSON.")
|
|
6652
|
+
.option("--limits-json <json>", "Optional limits JSON; platform hard ceilings still apply.")
|
|
6653
|
+
.option("--confirm-full-access", "Acknowledge standing full catalog access when the policy mode is full.")
|
|
6654
|
+
.option("--json", "Print a JSON envelope.")
|
|
6655
|
+
.action(async (slug, options) => {
|
|
6656
|
+
await handleAsyncAction("agent set", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}`, {
|
|
6657
|
+
method: "PATCH",
|
|
6658
|
+
body: {
|
|
6659
|
+
instructions: options.instructions,
|
|
6660
|
+
...(options.modelPolicyJson ? { model_policy: parseJsonObject(options.modelPolicyJson) } : {}),
|
|
6661
|
+
...(options.toolPolicyJson ? { tool_policy: parseJsonObject(options.toolPolicyJson) } : {}),
|
|
6662
|
+
...(options.contextPolicyJson ? { context_policy: parseJsonObject(options.contextPolicyJson) } : {}),
|
|
6663
|
+
...(options.skills ? { skill_slugs: options.skills.split(",").map((value) => value.trim()).filter(Boolean) } : {}),
|
|
6664
|
+
...(options.sandboxPolicyJson ? { sandbox_policy: parseJsonObject(options.sandboxPolicyJson) } : {}),
|
|
6665
|
+
...(options.limitsJson ? { limits: parseJsonObject(options.limitsJson) } : {}),
|
|
6666
|
+
confirm_full_access: options.confirmFullAccess === true,
|
|
6667
|
+
},
|
|
6668
|
+
}));
|
|
6669
|
+
}))
|
|
6670
|
+
.addCommand(new Command("create")
|
|
6671
|
+
.description("Create a custom agent and publish its first immutable version. Schedule it with `oxygen agent trigger create`.")
|
|
6672
|
+
.argument("<slug>", "User-minted agent slug: a lowercase letter then letters, digits, or hyphens (2-49 chars).")
|
|
6673
|
+
.requiredOption("--name <name>", "Display name for the agent.")
|
|
6674
|
+
.requiredOption("--instructions <text>", "Durable standing instructions for the first immutable version.")
|
|
6675
|
+
.option("--description <text>", "Optional one-line description of the agent's job.")
|
|
6676
|
+
.option("--model-policy-json <json>", "Model policy JSON. Defaults to managed OpenRouter medium.")
|
|
6677
|
+
.option("--tool-policy-json <json>", "Tool policy JSON. Omit restrictions for full eligible catalog access.")
|
|
6678
|
+
.option("--context-policy-json <json>", "Context policy JSON: on_demand or prefetch, optional resolve_args and file_ids.")
|
|
6679
|
+
.option("--skills <csv>", "Comma-separated product skill slugs snapshotted into this immutable version.")
|
|
6680
|
+
.option("--sandbox-policy-json <json>", "Sandbox policy JSON.")
|
|
6681
|
+
.option("--limits-json <json>", "Optional limits JSON; platform hard ceilings still apply.")
|
|
6682
|
+
.option("--confirm-full-access", "Acknowledge standing full catalog access when the policy mode is full.")
|
|
6683
|
+
.option("--json", "Print a JSON envelope.")
|
|
6684
|
+
.action(async (slug, options) => {
|
|
6685
|
+
await handleAsyncAction("agent create", options, () => requestOxygen("/api/cli/agent/create", {
|
|
6686
|
+
method: "POST",
|
|
6687
|
+
body: {
|
|
6688
|
+
slug,
|
|
6689
|
+
name: options.name,
|
|
6690
|
+
instructions: options.instructions,
|
|
6691
|
+
...(options.description ? { description: options.description } : {}),
|
|
6692
|
+
...(options.modelPolicyJson ? { model_policy: parseJsonObject(options.modelPolicyJson) } : {}),
|
|
6693
|
+
...(options.toolPolicyJson ? { tool_policy: parseJsonObject(options.toolPolicyJson) } : {}),
|
|
6694
|
+
...(options.contextPolicyJson ? { context_policy: parseJsonObject(options.contextPolicyJson) } : {}),
|
|
6695
|
+
...(options.skills ? { skill_slugs: options.skills.split(",").map((value) => value.trim()).filter(Boolean) } : {}),
|
|
6696
|
+
...(options.sandboxPolicyJson ? { sandbox_policy: parseJsonObject(options.sandboxPolicyJson) } : {}),
|
|
6697
|
+
...(options.limitsJson ? { limits: parseJsonObject(options.limitsJson) } : {}),
|
|
6698
|
+
confirm_full_access: options.confirmFullAccess === true,
|
|
6699
|
+
},
|
|
6700
|
+
}));
|
|
6701
|
+
}))
|
|
6702
|
+
.addCommand(new Command("custom")
|
|
6703
|
+
.description("List the org's user-defined custom agents (distinct from `agent list`, which shows the built-in roster).")
|
|
6704
|
+
.option("--json", "Print a JSON envelope.")
|
|
6705
|
+
.action(async (options) => {
|
|
6706
|
+
await handleAsyncAction("agent custom", options, () => requestOxygen("/api/cli/agent/custom"));
|
|
6707
|
+
}))
|
|
6708
|
+
.addCommand(new Command("run")
|
|
6709
|
+
.description("Start a durable Workspace Agent run on the Fly worker.")
|
|
6710
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6711
|
+
.option("--goal <text>", "Natural-language goal.")
|
|
6712
|
+
.option("--input-json <json>", "Structured run input instead of --goal.")
|
|
6713
|
+
.option("--idempotency-key <key>", "Deduplicate retries of the same source event.")
|
|
6714
|
+
.option("--thread-id <id>", "Continue an existing persistent Agent thread.")
|
|
6715
|
+
.option("--thread-mode <isolated|persistent>", "Create a persistent thread for this run. Defaults to isolated.")
|
|
6716
|
+
.option("--file-ids <csv>", "Comma-separated workspace file UUIDs attached to context and eligible for sandbox input.")
|
|
6717
|
+
.requiredOption("--max-credits <credits>", "Hard credit ceiling for this run.")
|
|
6718
|
+
.requiredOption("--approved", "Explicitly approve managed inference up to --max-credits.")
|
|
6719
|
+
.option("--json", "Print a JSON envelope.")
|
|
6720
|
+
.action(async (slug, options) => {
|
|
6721
|
+
await handleAsyncAction("agent run", options, () => {
|
|
6722
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
6723
|
+
if (maxCredits === undefined)
|
|
6724
|
+
throw new OxygenError("invalid_request", "--max-credits must be positive.", { exitCode: 2 });
|
|
6725
|
+
return requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/runs`, {
|
|
6726
|
+
method: "POST",
|
|
6727
|
+
body: {
|
|
6728
|
+
...(options.goal ? { goal: options.goal } : {}),
|
|
6729
|
+
...(options.inputJson ? { input: parseJsonObject(options.inputJson) } : {}),
|
|
6730
|
+
...(options.idempotencyKey ? { idempotency_key: options.idempotencyKey } : {}),
|
|
6731
|
+
...(options.threadId ? { thread_id: options.threadId } : {}),
|
|
6732
|
+
thread_mode: options.threadMode ?? "isolated",
|
|
6733
|
+
...(options.fileIds ? { file_ids: options.fileIds.split(",").map((value) => value.trim()).filter(Boolean) } : {}),
|
|
6734
|
+
max_credits: maxCredits,
|
|
6735
|
+
approved: options.approved === true,
|
|
6736
|
+
},
|
|
6737
|
+
});
|
|
6738
|
+
});
|
|
6739
|
+
}))
|
|
6740
|
+
.addCommand(new Command("run-get")
|
|
6741
|
+
.description("Inspect one Workspace Agent run and its append-only event ledger.")
|
|
6742
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6743
|
+
.argument("<run-id>", "Agent run id.")
|
|
6744
|
+
.option("--after-seq <n>", "Return events after this sequence number.")
|
|
6745
|
+
.option("--json", "Print a JSON envelope.")
|
|
6746
|
+
.action(async (slug, runId, options) => {
|
|
6747
|
+
await handleAsyncAction("agent run get", options, () => {
|
|
6748
|
+
const params = new URLSearchParams();
|
|
6749
|
+
if (options.afterSeq)
|
|
6750
|
+
params.set("after_seq", options.afterSeq);
|
|
6751
|
+
const suffix = params.toString() ? `?${params.toString()}` : "";
|
|
6752
|
+
return requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}${suffix}`);
|
|
6753
|
+
});
|
|
6754
|
+
}))
|
|
6755
|
+
.addCommand(new Command("cancel")
|
|
6756
|
+
.description("Request cancellation of an active Workspace Agent run.")
|
|
6757
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6758
|
+
.argument("<run-id>", "Agent run id.")
|
|
6759
|
+
.option("--json", "Print a JSON envelope.")
|
|
6760
|
+
.action(async (slug, runId, options) => {
|
|
6761
|
+
await handleAsyncAction("agent run cancel", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}/cancel`, { method: "POST" }));
|
|
6762
|
+
}))
|
|
6763
|
+
.addCommand(new Command("approval-decide")
|
|
6764
|
+
.description("Approve or reject one pending Workspace Agent tool call, then resume its durable run.")
|
|
6765
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6766
|
+
.argument("<run-id>", "Agent run id.")
|
|
6767
|
+
.argument("<approval-id>", "Approval id from the run event ledger.")
|
|
6768
|
+
.requiredOption("--decision <approved|rejected>", "Decision.")
|
|
6769
|
+
.option("--json", "Print a JSON envelope.")
|
|
6770
|
+
.action(async (slug, runId, approvalId, options) => {
|
|
6771
|
+
await handleAsyncAction("agent approval decide", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/runs/${encodeURIComponent(runId)}/approvals/${encodeURIComponent(approvalId)}`, { method: "POST", body: { decision: options.decision } }));
|
|
6772
|
+
}))
|
|
6773
|
+
.addCommand(new Command("trigger-create")
|
|
6774
|
+
.description("Create a cron, internal event, or signed-webhook trigger for the Workspace Agent.")
|
|
6775
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6776
|
+
.requiredOption("--type <cron|event|webhook>", "Trigger type.")
|
|
6777
|
+
.requiredOption("--instruction <text>", "Goal template run for each delivery.")
|
|
6778
|
+
.option("--cron <expression>", "Five-field cron expression for cron triggers.")
|
|
6779
|
+
.option("--timezone <iana>", "IANA timezone. Defaults to UTC.")
|
|
6780
|
+
.option("--event-source <source>", "Internal workflow-event source for event triggers.")
|
|
6781
|
+
.option("--event-type <event>", "Internal workflow-event type for event triggers.")
|
|
6782
|
+
.option("--thread-mode <isolated|persistent>", "Reuse a bounded thread across deliveries. Defaults to isolated.")
|
|
6783
|
+
.requiredOption("--max-credits <credits>", "Standing per-delivery credit ceiling.")
|
|
6784
|
+
.requiredOption("--approved", "Authorize each trigger delivery up to --max-credits.")
|
|
6785
|
+
.option("--json", "Print a JSON envelope.")
|
|
6786
|
+
.action(async (slug, options) => {
|
|
6787
|
+
await handleAsyncAction("agent trigger create", options, () => {
|
|
6788
|
+
const maxCredits = readPositiveNumber(options.maxCredits);
|
|
6789
|
+
if (maxCredits === undefined)
|
|
6790
|
+
throw new OxygenError("invalid_request", "--max-credits must be positive.", { exitCode: 2 });
|
|
6791
|
+
return requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/triggers`, { method: "POST", body: {
|
|
6792
|
+
type: options.type, instruction_template: options.instruction,
|
|
6793
|
+
...(options.cron ? { cron_expression: options.cron } : {}),
|
|
6794
|
+
...(options.timezone ? { timezone: options.timezone } : {}),
|
|
6795
|
+
...(options.eventSource ? { event_source: options.eventSource } : {}),
|
|
6796
|
+
...(options.eventType ? { event_type: options.eventType } : {}),
|
|
6797
|
+
thread_mode: options.threadMode ?? "isolated",
|
|
6798
|
+
max_credits: maxCredits,
|
|
6799
|
+
approved: options.approved === true,
|
|
6800
|
+
} });
|
|
6801
|
+
});
|
|
6802
|
+
}))
|
|
6803
|
+
.addCommand(new Command("triggers")
|
|
6804
|
+
.description("List Workspace Agent cron, internal event, and webhook triggers.")
|
|
6805
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6806
|
+
.option("--json", "Print a JSON envelope.")
|
|
6807
|
+
.action(async (slug, options) => {
|
|
6808
|
+
await handleAsyncAction("agent triggers", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/triggers`));
|
|
6809
|
+
}))
|
|
6810
|
+
.addCommand(new Command("trigger-set")
|
|
6811
|
+
.description("Activate, pause, or archive one Workspace Agent trigger.")
|
|
6812
|
+
.argument("<slug>", "Currently: workspace.")
|
|
6813
|
+
.argument("<trigger-id>", "Agent trigger id.")
|
|
6814
|
+
.requiredOption("--status <active|paused|archived>", "New trigger status.")
|
|
6815
|
+
.option("--json", "Print a JSON envelope.")
|
|
6816
|
+
.action(async (slug, triggerId, options) => {
|
|
6817
|
+
await handleAsyncAction("agent trigger set", options, () => requestOxygen(`/api/cli/agent/${encodeURIComponent(slug)}/triggers/${encodeURIComponent(triggerId)}`, { method: "PATCH", body: { status: options.status } }));
|
|
6818
|
+
}))
|
|
6819
|
+
.addCommand(new Command("file-create")
|
|
6820
|
+
.description("Store a tenant-owned text file for Agent context and sandbox input.")
|
|
6821
|
+
.requiredOption("--name <name>", "Workspace filename.")
|
|
6822
|
+
.requiredOption("--text <text>", "Inline text content (max 1.5 MB).")
|
|
6823
|
+
.option("--mime-type <type>", "MIME type. Defaults to text/plain.")
|
|
6824
|
+
.option("--json", "Print a JSON envelope.")
|
|
6825
|
+
.action(async (options) => {
|
|
6826
|
+
await handleAsyncAction("agent file create", options, () => requestOxygen("/api/cli/agent/files", {
|
|
6827
|
+
method: "POST", body: { name: options.name, text: options.text, mime_type: options.mimeType ?? "text/plain" },
|
|
6828
|
+
}));
|
|
6829
|
+
}))
|
|
6830
|
+
.addCommand(new Command("files")
|
|
6831
|
+
.description("List tenant-owned Agent context files.")
|
|
6832
|
+
.option("--limit <n>", "Maximum files (1-500).")
|
|
6833
|
+
.option("--json", "Print a JSON envelope.")
|
|
6834
|
+
.action(async (options) => {
|
|
6835
|
+
await handleAsyncAction("agent files", options, () => {
|
|
6836
|
+
const limit = readPositiveInt(options.limit);
|
|
6837
|
+
return requestOxygen(`/api/cli/agent/files${limit ? `?limit=${limit}` : ""}`);
|
|
6838
|
+
});
|
|
6585
6839
|
}));
|
|
6586
6840
|
program
|
|
6587
6841
|
.command("copilot")
|
|
@@ -6594,6 +6848,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6594
6848
|
.option("--per-turn-ceiling <number>", "Optional per-turn credit ceiling that caps any single turn's inference spend.")
|
|
6595
6849
|
.option("--journey <slug>", "Optional journey slug to seed the session's goal and context.")
|
|
6596
6850
|
.option("--title <text>", "Optional human title for the session.")
|
|
6851
|
+
.option("--auto-approve", "Start with auto-approve ON for paid, workspace-internal actions (cards are still created and decided automatically within each action's credit cap). External sends, enrollments, publishes, DNS, and external CRM pushes always stay human-gated.")
|
|
6597
6852
|
.option("--json", "Print a JSON envelope.")
|
|
6598
6853
|
.action(async (options) => {
|
|
6599
6854
|
await handleAsyncAction("copilot start", options, () => {
|
|
@@ -6617,6 +6872,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
6617
6872
|
body.journey = journey;
|
|
6618
6873
|
if (title)
|
|
6619
6874
|
body.title = title;
|
|
6875
|
+
if (options.autoApprove)
|
|
6876
|
+
body.auto_approve = true;
|
|
6620
6877
|
return requestOxygen("/api/cli/copilot/sessions", { method: "POST", body });
|
|
6621
6878
|
});
|
|
6622
6879
|
}))
|
|
@@ -7377,7 +7634,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7377
7634
|
await handleAsyncAction("senders checkpoints", options, () => requestOxygen("/api/cli/senders/checkpoints"));
|
|
7378
7635
|
}))
|
|
7379
7636
|
.addCommand(new Command("connect")
|
|
7380
|
-
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). Use --count to mint several links at once for bulk onboarding. Open each URL in a browser to complete authentication.")
|
|
7637
|
+
.description("Get a Unipile hosted-auth URL to connect a new LinkedIn account (or reconnect with --reconnect). Use --count to mint several links at once for bulk onboarding. Open each URL in a browser to complete authentication. Links are shareable: send one to the account owner (e.g. a client) — no Oxygen login is needed to complete it, and the account lands in this workspace's sender list. Each link is valid for 30 minutes and connects one account.")
|
|
7381
7638
|
.option("--reconnect <connection_id>", "Reconnect an existing connection instead of creating a new one. Accepts a connection id.")
|
|
7382
7639
|
.option("--sales-nav", "Request Classic + Sales Navigator access during Unipile hosted authentication.")
|
|
7383
7640
|
.option("--count <n>", "Mint N hosted-auth links in one call for bulk onboarding (1-25, default 1). Each link connects a different account. Ignored when reconnecting.")
|
|
@@ -7402,6 +7659,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
7402
7659
|
.option("--json", "Print a JSON envelope.")
|
|
7403
7660
|
.action(async (id, options) => {
|
|
7404
7661
|
await handleAsyncAction("senders get", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}`));
|
|
7662
|
+
}))
|
|
7663
|
+
.addCommand(new Command("tag")
|
|
7664
|
+
.description("Replace a sender account's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the senders behind one campaign tag. See `oxygen tags list` for the vocabulary.")
|
|
7665
|
+
.argument("<id>", "Sender account id or Unipile account id.")
|
|
7666
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
7667
|
+
.option("--json", "Print a JSON envelope.")
|
|
7668
|
+
.action(async (id, options) => {
|
|
7669
|
+
await handleAsyncAction("senders tag", options, () => requestOxygen(`/api/cli/senders/${encodeURIComponent(id)}/tags`, {
|
|
7670
|
+
method: "POST",
|
|
7671
|
+
body: { tags: splitCommaList(options.tags) },
|
|
7672
|
+
}));
|
|
7405
7673
|
}))
|
|
7406
7674
|
.addCommand(new Command("health")
|
|
7407
7675
|
.description("Show a one-call health snapshot for a LinkedIn sender: status, the latest error reason, any open security checkpoint, today's usage, the warm-up ramp, and when the daily quota resets. Read-only — no provider call, no credits. <id> accepts a sender account id, connection id, or Unipile account id.")
|
|
@@ -8147,6 +8415,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8147
8415
|
.option("--bucket <bucket>", "Email only: primary or others (superseded by --segment).")
|
|
8148
8416
|
.option("--segment <segment>", "Email only: top-tab folder — primary, others, sent, warmup, or dmarc.")
|
|
8149
8417
|
.option("--status <keys>", "Comma-separated status keys (e.g. interested,meeting_booked). Cross-channel — filters email + LinkedIn + WhatsApp by the shared taxonomy.")
|
|
8418
|
+
.option("--sentiment <values>", "Comma-separated AI sentiment: positive, neutral, negative. Cross-channel; not-yet-analyzed conversations never match.")
|
|
8419
|
+
.option("--tag <tags>", "Comma-separated campaign tags — matches email conversations whose campaign (sequence) carries any of these tags. DMs have no campaign link, so a tag filter shows email only.")
|
|
8150
8420
|
.option("--since <iso>", "Only conversations whose last message is on/after this ISO date/timestamp. Cross-channel.")
|
|
8151
8421
|
.option("--until <iso>", "Only conversations whose last message is on/before this ISO date/timestamp. Cross-channel.")
|
|
8152
8422
|
.option("--sequence-id <ids>", "Email only: comma-separated campaign (sequence) UUIDs (not slugs — get the id from `sequences get <slug>`).")
|
|
@@ -8157,6 +8427,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8157
8427
|
.option("--search <text>", "Filter by attendee name or last-message text. Cross-channel.")
|
|
8158
8428
|
.option("--include-archived", "Include archived conversations.")
|
|
8159
8429
|
.option("--limit <n>", "Maximum conversations to return (1-200). Defaults to 50.")
|
|
8430
|
+
.option("--cursor <cursor>", "channel=all only: the previous page's next_cursor — resumes the merged stream after that row.")
|
|
8431
|
+
.option("--no-counts", "channel=all only: skip the sidebar facet counts for a faster paged read (total_conversations/unread_conversations/sidebar_counts omitted from the envelope). Other channels ignore it.")
|
|
8160
8432
|
.option("--json", "Print a JSON envelope.")
|
|
8161
8433
|
.action(async (options) => {
|
|
8162
8434
|
await handleAsyncAction("inbox list", options, () => {
|
|
@@ -8176,6 +8448,8 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8176
8448
|
["segment", "segment"],
|
|
8177
8449
|
["channels", "channels"],
|
|
8178
8450
|
["status", "status"],
|
|
8451
|
+
["sentiment", "sentiment"],
|
|
8452
|
+
["tag", "tag"],
|
|
8179
8453
|
["since", "since"],
|
|
8180
8454
|
["until", "until"],
|
|
8181
8455
|
["sequenceId", "sequence_id"],
|
|
@@ -8183,6 +8457,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8183
8457
|
["domain", "domain"],
|
|
8184
8458
|
["excludeDomain", "exclude_domain"],
|
|
8185
8459
|
["mailboxId", "mailbox_id"],
|
|
8460
|
+
["cursor", "cursor"],
|
|
8186
8461
|
]) {
|
|
8187
8462
|
const value = readOption(options[flag]);
|
|
8188
8463
|
if (value)
|
|
@@ -8196,6 +8471,10 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8196
8471
|
const limit = readOption(options.limit);
|
|
8197
8472
|
if (limit)
|
|
8198
8473
|
params.set("limit", limit);
|
|
8474
|
+
// Commander's --no-counts sets counts:false; absent leaves the
|
|
8475
|
+
// server default (counts included) untouched.
|
|
8476
|
+
if (options.counts === false)
|
|
8477
|
+
params.set("include_counts", "false");
|
|
8199
8478
|
const suffix = params.toString();
|
|
8200
8479
|
return requestOxygen(`/api/cli/inbox${suffix ? `?${suffix}` : ""}`);
|
|
8201
8480
|
});
|
|
@@ -8342,6 +8621,31 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8342
8621
|
body: { status, channel },
|
|
8343
8622
|
});
|
|
8344
8623
|
});
|
|
8624
|
+
}))
|
|
8625
|
+
.addCommand(new Command("tag")
|
|
8626
|
+
.description("Add/remove workspace tags on a conversation (Tags primitive — the same vocabulary as sequences, tables, posts; see `oxygen tags list`). Delta semantics: --add unions, --remove subtracts, remove wins on conflict. Internal metadata, never sent to the counterpart. --channel linkedin/whatsapp tags a DM conversation.")
|
|
8627
|
+
.argument("<conversation>", "Conversation id, Unipile chat id, or (email) Zapbox thread id.")
|
|
8628
|
+
.option("--add <tags>", "Comma-separated tags to add.")
|
|
8629
|
+
.option("--remove <tags>", "Comma-separated tags to remove.")
|
|
8630
|
+
.option("--channel <channel>", "Inbox channel: email (default), linkedin, or whatsapp.")
|
|
8631
|
+
.option("--json", "Print a JSON envelope.")
|
|
8632
|
+
.action(async (conversation, options) => {
|
|
8633
|
+
await handleAsyncAction("inbox tag", options, () => {
|
|
8634
|
+
const add = splitCommaList(options.add);
|
|
8635
|
+
const remove = splitCommaList(options.remove);
|
|
8636
|
+
if (add.length === 0 && remove.length === 0) {
|
|
8637
|
+
throw new OxygenError("invalid_request", "Pass --add and/or --remove.", { exitCode: 1 });
|
|
8638
|
+
}
|
|
8639
|
+
const channel = readOption(options.channel) ?? "email";
|
|
8640
|
+
return requestOxygen(`/api/cli/inbox/${encodeURIComponent(conversation)}/tags`, {
|
|
8641
|
+
method: "POST",
|
|
8642
|
+
body: {
|
|
8643
|
+
...(add.length > 0 ? { add } : {}),
|
|
8644
|
+
...(remove.length > 0 ? { remove } : {}),
|
|
8645
|
+
channel,
|
|
8646
|
+
},
|
|
8647
|
+
});
|
|
8648
|
+
});
|
|
8345
8649
|
}))
|
|
8346
8650
|
.addCommand(new Command("rescan")
|
|
8347
8651
|
.description("Re-queue conversations for AI re-classification (status + sentiment + drafted reply). Without --yes, previews the counts that would be re-flipped. With --yes, the worker re-classifies the pending rows (free for opt-outs, low-tier model otherwise, credit-capped).")
|
|
@@ -8562,6 +8866,37 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8562
8866
|
body.max_drafts_per_day = Number(options.maxDraftsPerDay);
|
|
8563
8867
|
return requestOxygen("/api/cli/inbox/reply-agent", { method: "POST", body });
|
|
8564
8868
|
});
|
|
8869
|
+
})))
|
|
8870
|
+
.addCommand(new Command("auto-tagger")
|
|
8871
|
+
.description("The auto-tagging agent (the 'AI Inbox Tagger'): classifies each new inbound conversation against your workspace tag allowlist and auto-applies matches — additive, never re-adds a tag you removed, zero extra model calls (rides the analysis pass). Roster + run history: `oxygen agent get inbox-auto-tagger`; web home: /agents/inbox-auto-tagger.")
|
|
8872
|
+
.addCommand(new Command("get")
|
|
8873
|
+
.description("Show the auto-tagger config (enabled, tag allowlist, channels).")
|
|
8874
|
+
.option("--json", "Print a JSON envelope.")
|
|
8875
|
+
.action(async (options) => {
|
|
8876
|
+
await handleAsyncAction("inbox auto-tagger get", options, () => requestOxygen("/api/cli/inbox/auto-tagger"));
|
|
8877
|
+
}))
|
|
8878
|
+
.addCommand(new Command("set")
|
|
8879
|
+
.description("Update the auto-tagger. --tags replaces the whole allowlist (max 50; `--tags \"\"` clears, which pauses tagging while keeping the agent enabled).")
|
|
8880
|
+
.option("--enabled", "Enable the agent.")
|
|
8881
|
+
.option("--disabled", "Disable the agent.")
|
|
8882
|
+
.option("--tags <tags>", "Comma-separated tag allowlist the classifier may apply (replaces the whole set).")
|
|
8883
|
+
.option("--channels <list>", "Comma-separated channels to tag: a non-empty subset of email, linkedin, whatsapp (default all three).")
|
|
8884
|
+
.option("--json", "Print a JSON envelope.")
|
|
8885
|
+
.action(async (options) => {
|
|
8886
|
+
await handleAsyncAction("inbox auto-tagger set", options, () => {
|
|
8887
|
+
const body = {};
|
|
8888
|
+
if (options.enabled)
|
|
8889
|
+
body.enabled = true;
|
|
8890
|
+
if (options.disabled)
|
|
8891
|
+
body.enabled = false;
|
|
8892
|
+
if (options.tags !== undefined) {
|
|
8893
|
+
body.tags = options.tags.split(",").map((tag) => tag.trim()).filter(Boolean);
|
|
8894
|
+
}
|
|
8895
|
+
if (options.channels !== undefined) {
|
|
8896
|
+
body.channels = options.channels.split(",").map((c) => c.trim()).filter(Boolean);
|
|
8897
|
+
}
|
|
8898
|
+
return requestOxygen("/api/cli/inbox/auto-tagger", { method: "POST", body });
|
|
8899
|
+
});
|
|
8565
8900
|
}))));
|
|
8566
8901
|
program.addCommand(new Command("messages")
|
|
8567
8902
|
.description("Cross-channel message corpus: every individual email + LinkedIn + WhatsApp message as one searchable stream (Postgres full-text search over bodies, keyset paginated by recency), plus reply-rate/campaign analytics. Message-level, unlike inbox (conversation-level triage).")
|
|
@@ -8576,6 +8911,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8576
8911
|
.option("--sequence-id <id>", "Email only: campaign (sequence) id (restricts the result to email).")
|
|
8577
8912
|
.option("--status <keys>", "Comma-separated conversation status keys (e.g. interested,meeting_booked).")
|
|
8578
8913
|
.option("--sentiment <sentiment>", "Filter by sentiment: positive, neutral, or negative.")
|
|
8914
|
+
.option("--tag <tag>", "Only messages whose conversation carries this workspace tag (see `oxygen tags list`).")
|
|
8579
8915
|
.option("--since <iso>", "Only messages sent at or after this ISO timestamp.")
|
|
8580
8916
|
.option("--until <iso>", "Only messages sent before this ISO timestamp.")
|
|
8581
8917
|
.option("--limit <n>", "Maximum messages to return (1-200). Defaults to 50.")
|
|
@@ -8596,6 +8932,7 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
8596
8932
|
["sequenceId", "sequence_id"],
|
|
8597
8933
|
["status", "status"],
|
|
8598
8934
|
["sentiment", "sentiment"],
|
|
8935
|
+
["tag", "tag"],
|
|
8599
8936
|
["since", "since"],
|
|
8600
8937
|
["until", "until"],
|
|
8601
8938
|
["limit", "limit"],
|
|
@@ -9496,6 +9833,17 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
9496
9833
|
body: { mailboxes: parsed.mailboxes ?? [] },
|
|
9497
9834
|
});
|
|
9498
9835
|
});
|
|
9836
|
+
}))
|
|
9837
|
+
.addCommand(new Command("tag")
|
|
9838
|
+
.description("Replace a sending mailbox's workspace tags (whole set; `--tags \"\"` clears) — e.g. pool the mailboxes behind one campaign tag. See `oxygen tags list` for the vocabulary.")
|
|
9839
|
+
.argument("<mailbox>", "Mailbox id or email address.")
|
|
9840
|
+
.requiredOption("--tags <tags>", "Comma-separated workspace tags (replaces the whole set; empty clears).")
|
|
9841
|
+
.option("--json", "Print a JSON envelope.")
|
|
9842
|
+
.action(async (mailbox, options) => {
|
|
9843
|
+
await handleAsyncAction("mailboxes tag", options, () => requestOxygen(`/api/cli/mailboxes/${encodeURIComponent(mailbox)}/tags`, {
|
|
9844
|
+
method: "POST",
|
|
9845
|
+
body: { tags: splitCommaList(options.tags) },
|
|
9846
|
+
}));
|
|
9499
9847
|
}))
|
|
9500
9848
|
.addCommand(new Command("status")
|
|
9501
9849
|
.description("Set a sending mailbox's status. Pausing/disabling takes the inbox out of the rotation pool without losing its warmup state.")
|
|
@@ -10117,10 +10465,18 @@ fixed scan; watermark_cursor starts a later bounded replay. Consumers must retai
|
|
|
10117
10465
|
.addCommand(new Command("list")
|
|
10118
10466
|
.description("List workflow automations. Archived workflows are hidden unless you pass --include-archived.")
|
|
10119
10467
|
.option("--include-archived", "Also list archived workflows, which are hidden by default.")
|
|
10468
|
+
.option("--tag <tag>", "Only workflows carrying this workspace tag (see `oxygen tags list`).")
|
|
10120
10469
|
.option("--json", "Print a JSON envelope.")
|
|
10121
10470
|
.action(async (options) => {
|
|
10122
10471
|
await handleAsyncAction("workflows list", options, async () => {
|
|
10123
|
-
const
|
|
10472
|
+
const params = new URLSearchParams();
|
|
10473
|
+
if (options.includeArchived)
|
|
10474
|
+
params.set("include_archived", "true");
|
|
10475
|
+
const tag = readOption(options.tag);
|
|
10476
|
+
if (tag)
|
|
10477
|
+
params.set("tag", tag);
|
|
10478
|
+
const qs = params.toString() ? `?${params.toString()}` : "";
|
|
10479
|
+
const data = await requestOxygen(`/api/cli/workflows${qs}`);
|
|
10124
10480
|
writeDisabledWorkflowNotices(data);
|
|
10125
10481
|
return data;
|
|
10126
10482
|
});
|
|
@@ -57,6 +57,8 @@ export type CreditTopupPackId = CreditTopupPack["id"];
|
|
|
57
57
|
export declare function resolveCreditTopupPack(id: string | null | undefined): CreditTopupPack | null;
|
|
58
58
|
export declare const TRIAL_PERIOD_DAYS = 7;
|
|
59
59
|
export declare const TRIAL_CREDIT_GRANT = 20000;
|
|
60
|
+
export declare const WALKTHROUGH_COMPLETION_BONUS_CREDITS = 1000;
|
|
61
|
+
export declare const WALKTHROUGH_SCRAPE_MAX_CREDITS = 2000;
|
|
60
62
|
export declare const BASE_PRICING_PLANS: {
|
|
61
63
|
readonly free: {
|
|
62
64
|
readonly tier: "free";
|
|
@@ -47,6 +47,13 @@ export function resolveCreditTopupPack(id) {
|
|
|
47
47
|
// credit exhaustion or when the trial clock elapses, whichever comes first.
|
|
48
48
|
export const TRIAL_PERIOD_DAYS = 7;
|
|
49
49
|
export const TRIAL_CREDIT_GRANT = 20_000;
|
|
50
|
+
// Onboarding walkthrough (2026-07-21): completing the in-product walkthrough
|
|
51
|
+
// grants a one-off bonus (idempotent per billing owner), and the walkthrough's
|
|
52
|
+
// "start scraping engagers" step may spend at most this many credits on the
|
|
53
|
+
// managed cookieless scraper (founder-approved budget; the rest of the kickoff
|
|
54
|
+
// is free connected-account reads).
|
|
55
|
+
export const WALKTHROUGH_COMPLETION_BONUS_CREDITS = 1_000;
|
|
56
|
+
export const WALKTHROUGH_SCRAPE_MAX_CREDITS = 2_000;
|
|
50
57
|
export const BASE_PRICING_PLANS = {
|
|
51
58
|
free: {
|
|
52
59
|
tier: "free",
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
* can re-source the catalog without dragging in the shared barrel.
|
|
22
22
|
*/
|
|
23
23
|
export type HostedAiLevel = "low" | "medium" | "high";
|
|
24
|
-
export type HostedAiUseCase = "ai_column" | "copilot";
|
|
24
|
+
export type HostedAiUseCase = "ai_column" | "copilot" | "agent";
|
|
25
25
|
export type HostedAiModelSpec = {
|
|
26
26
|
/** OpenRouter model id, e.g. "deepseek/deepseek-v4-flash". */
|
|
27
27
|
model: string;
|
|
@@ -47,6 +47,7 @@ export type HostedAiModelSpec = {
|
|
|
47
47
|
export declare const HOSTED_AI_MODEL_REGISTRY: Record<HostedAiUseCase, Record<HostedAiLevel, HostedAiModelSpec>>;
|
|
48
48
|
/** The reasoning tier the hosted copilot runs at by default. */
|
|
49
49
|
export declare const COPILOT_DEFAULT_LEVEL: HostedAiLevel;
|
|
50
|
+
export declare const AGENT_DEFAULT_LEVEL: HostedAiLevel;
|
|
50
51
|
/**
|
|
51
52
|
* Resolve the model spec for a hosted-AI use case + reasoning level.
|
|
52
53
|
*
|
|
@@ -77,9 +77,38 @@ export const HOSTED_AI_MODEL_REGISTRY = {
|
|
|
77
77
|
maxOutputTokens: 8192,
|
|
78
78
|
},
|
|
79
79
|
},
|
|
80
|
+
agent: {
|
|
81
|
+
low: {
|
|
82
|
+
model: "deepseek/deepseek-v4-flash",
|
|
83
|
+
displayName: "DeepSeek V4 Flash",
|
|
84
|
+
fallbackModels: [],
|
|
85
|
+
reasoningEffort: "medium",
|
|
86
|
+
estPromptUsdPerM: 0.09,
|
|
87
|
+
estCompletionUsdPerM: 0.18,
|
|
88
|
+
maxOutputTokens: 8192,
|
|
89
|
+
},
|
|
90
|
+
medium: {
|
|
91
|
+
model: "deepseek/deepseek-v4-pro",
|
|
92
|
+
displayName: "DeepSeek V4 Pro",
|
|
93
|
+
fallbackModels: ["deepseek/deepseek-v4-flash"],
|
|
94
|
+
reasoningEffort: "high",
|
|
95
|
+
estPromptUsdPerM: 0.435,
|
|
96
|
+
estCompletionUsdPerM: 0.87,
|
|
97
|
+
maxOutputTokens: 8192,
|
|
98
|
+
},
|
|
99
|
+
high: {
|
|
100
|
+
model: "moonshotai/kimi-k2.6",
|
|
101
|
+
displayName: "Kimi K2.6",
|
|
102
|
+
fallbackModels: ["deepseek/deepseek-v4-pro"],
|
|
103
|
+
estPromptUsdPerM: 0.66,
|
|
104
|
+
estCompletionUsdPerM: 3.41,
|
|
105
|
+
maxOutputTokens: 8192,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
80
108
|
};
|
|
81
109
|
/** The reasoning tier the hosted copilot runs at by default. */
|
|
82
110
|
export const COPILOT_DEFAULT_LEVEL = "high";
|
|
111
|
+
export const AGENT_DEFAULT_LEVEL = "medium";
|
|
83
112
|
const LEVEL_ENV_SUFFIX = {
|
|
84
113
|
low: "LOW",
|
|
85
114
|
medium: "MEDIUM",
|
|
@@ -134,12 +163,13 @@ export function findHostedAiModelSpec(model, preferredUseCase) {
|
|
|
134
163
|
}
|
|
135
164
|
export function resolveHostedAiModel(input) {
|
|
136
165
|
const base = HOSTED_AI_MODEL_REGISTRY[input.useCase][input.level];
|
|
137
|
-
if (input.useCase
|
|
166
|
+
if (input.useCase === "ai_column")
|
|
138
167
|
return base;
|
|
139
168
|
const env = input.env ?? process.env;
|
|
140
169
|
const suffix = LEVEL_ENV_SUFFIX[input.level];
|
|
141
|
-
const
|
|
142
|
-
const
|
|
170
|
+
const envPrefix = input.useCase === "agent" ? "OXYGEN_AGENT" : "OXYGEN_COPILOT";
|
|
171
|
+
const modelOverride = readTrimmedEnv(env, `${envPrefix}_MODEL_${suffix}`);
|
|
172
|
+
const rawFallback = readTrimmedEnv(env, `${envPrefix}_FALLBACK_${suffix}`);
|
|
143
173
|
const fallbackOverride = rawFallback === undefined ? undefined : parseFallbackCsv(rawFallback);
|
|
144
174
|
if (modelOverride === undefined && fallbackOverride === undefined)
|
|
145
175
|
return base;
|
|
@@ -20,6 +20,7 @@ export * from "./knowledge-constants.js";
|
|
|
20
20
|
export * from "./knowledge-links.js";
|
|
21
21
|
export * from "./knowledge-markdown.js";
|
|
22
22
|
export * from "./knowledge-seed-content.js";
|
|
23
|
+
export * from "./langfuse.js";
|
|
23
24
|
export * from "./linkedin-mentions.js";
|
|
24
25
|
export * from "./linkedin-post-url.js";
|
|
25
26
|
export * from "./linkedin-url.js";
|
|
@@ -30,6 +31,7 @@ export * from "./sequence-template.js";
|
|
|
30
31
|
export * from "./sequences.js";
|
|
31
32
|
export * from "./suppression-entries.js";
|
|
32
33
|
export * from "./log.js";
|
|
34
|
+
export { sanitizeLogFields } from "./redaction.js";
|
|
33
35
|
export * from "./provider-request-outcomes.js";
|
|
34
36
|
export * from "./schedule-label.js";
|
|
35
37
|
export * from "./signup-lead-deliveries.js";
|
|
@@ -20,6 +20,7 @@ export * from "./knowledge-constants.js";
|
|
|
20
20
|
export * from "./knowledge-links.js";
|
|
21
21
|
export * from "./knowledge-markdown.js";
|
|
22
22
|
export * from "./knowledge-seed-content.js";
|
|
23
|
+
export * from "./langfuse.js";
|
|
23
24
|
export * from "./linkedin-mentions.js";
|
|
24
25
|
export * from "./linkedin-post-url.js";
|
|
25
26
|
export * from "./linkedin-url.js";
|
|
@@ -30,6 +31,11 @@ export * from "./sequence-template.js";
|
|
|
30
31
|
export * from "./sequences.js";
|
|
31
32
|
export * from "./suppression-entries.js";
|
|
32
33
|
export * from "./log.js";
|
|
34
|
+
// Narrow, deliberate export (ADR 0014): lets telemetry emitters regression-test
|
|
35
|
+
// their field names against the REAL log sanitizer — the unanchored
|
|
36
|
+
// SECRET_KEY_PATTERN redacts any name containing "token", which mocked loggers
|
|
37
|
+
// cannot catch. Not a license to pre-sanitize outside log().
|
|
38
|
+
export { sanitizeLogFields } from "./redaction.js";
|
|
33
39
|
export * from "./provider-request-outcomes.js";
|
|
34
40
|
export * from "./schedule-label.js";
|
|
35
41
|
export * from "./signup-lead-deliveries.js";
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { Langfuse } from "langfuse";
|
|
2
|
+
type EnvMap = Record<string, string | undefined>;
|
|
3
|
+
export type LlmObservationLevel = "DEBUG" | "DEFAULT" | "WARNING" | "ERROR";
|
|
4
|
+
export type LlmTraceBody = {
|
|
5
|
+
id: string;
|
|
6
|
+
name: string;
|
|
7
|
+
sessionId?: string | null;
|
|
8
|
+
userId?: string | null;
|
|
9
|
+
input?: unknown;
|
|
10
|
+
output?: unknown;
|
|
11
|
+
metadata?: Record<string, unknown>;
|
|
12
|
+
tags?: string[];
|
|
13
|
+
};
|
|
14
|
+
export type LlmSpanBody = {
|
|
15
|
+
id: string;
|
|
16
|
+
traceId: string;
|
|
17
|
+
name: string;
|
|
18
|
+
input?: unknown;
|
|
19
|
+
output?: unknown;
|
|
20
|
+
metadata?: Record<string, unknown>;
|
|
21
|
+
startTime?: Date;
|
|
22
|
+
endTime?: Date;
|
|
23
|
+
level?: LlmObservationLevel;
|
|
24
|
+
statusMessage?: string | null;
|
|
25
|
+
};
|
|
26
|
+
export type LlmGenerationBody = LlmSpanBody & {
|
|
27
|
+
model?: string | null;
|
|
28
|
+
completionStartTime?: Date | null;
|
|
29
|
+
usageDetails?: Record<string, number>;
|
|
30
|
+
costDetails?: Record<string, number>;
|
|
31
|
+
};
|
|
32
|
+
export type LlmEventBody = {
|
|
33
|
+
id: string;
|
|
34
|
+
traceId: string;
|
|
35
|
+
name: string;
|
|
36
|
+
input?: unknown;
|
|
37
|
+
metadata?: Record<string, unknown>;
|
|
38
|
+
startTime?: Date;
|
|
39
|
+
};
|
|
40
|
+
export type LlmTracingClient = {
|
|
41
|
+
trace(body: LlmTraceBody): void;
|
|
42
|
+
span(body: LlmSpanBody): void;
|
|
43
|
+
generation(body: LlmGenerationBody): void;
|
|
44
|
+
event(body: LlmEventBody): void;
|
|
45
|
+
/** Never rejects; bounded at ~5s. */
|
|
46
|
+
flush(): Promise<void>;
|
|
47
|
+
/** Flush + stop background timers. Never rejects; bounded at ~5s. */
|
|
48
|
+
shutdown(): Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
/**
|
|
51
|
+
* FAIL CLOSED: LLM tracing is on only when OXYGEN_LLM_TRACING_ENABLED is exactly
|
|
52
|
+
* "1"/"true" (trimmed, case-insensitive) AND both Langfuse keys are present.
|
|
53
|
+
*/
|
|
54
|
+
export declare function isLlmTracingEnabled(env?: EnvMap): boolean;
|
|
55
|
+
export declare function resolveLlmTracingEnvironment(env?: EnvMap): string;
|
|
56
|
+
type LangfuseLike = Pick<Langfuse, "trace" | "span" | "generation" | "event" | "flushAsync" | "shutdownAsync"> & {
|
|
57
|
+
on?: (event: string, listener: (...args: unknown[]) => void) => void;
|
|
58
|
+
};
|
|
59
|
+
/**
|
|
60
|
+
* Construct a fail-open Langfuse client, or `null` when tracing is disabled or
|
|
61
|
+
* misconfigured. Prefer the process-wide `getLlmTracingClient` in app code;
|
|
62
|
+
* this direct factory exists for tests (inject `langfuseImpl`).
|
|
63
|
+
*/
|
|
64
|
+
export declare function createLlmTracingClient(env?: EnvMap, options?: {
|
|
65
|
+
langfuseImpl?: LangfuseLike;
|
|
66
|
+
}): LlmTracingClient | null;
|
|
67
|
+
export declare function getLlmTracingClient(env?: EnvMap): LlmTracingClient | null;
|
|
68
|
+
/** Flush the singleton if it exists. Never rejects. Hang off request/cycle ends. */
|
|
69
|
+
export declare function flushLlmTracing(env?: EnvMap): Promise<void>;
|
|
70
|
+
type FlushScheduler = (task: () => Promise<void>) => void;
|
|
71
|
+
export declare function setLlmTracingFlushScheduler(scheduler: FlushScheduler | null): void;
|
|
72
|
+
/** Request-safe flush trigger: post-response on web, fire-and-forget elsewhere. */
|
|
73
|
+
export declare function scheduleLlmTracingFlush(env?: EnvMap): void;
|
|
74
|
+
/** Shutdown the singleton (worker exit). Never rejects. */
|
|
75
|
+
export declare function shutdownLlmTracing(env?: EnvMap): Promise<void>;
|
|
76
|
+
export declare function __resetLlmTracingForTests(): void;
|
|
77
|
+
export {};
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
// LLM-observability transport (ADR 0014): Langfuse is the ONE sanctioned store
|
|
2
|
+
// for full prompt/completion/tool-IO payloads. Axiom stays metadata-only (log.ts
|
|
3
|
+
// redaction drops prompt/input/output-named fields BY DESIGN — that boundary is
|
|
4
|
+
// unchanged), and PostHog stays sanitized product analytics. This module
|
|
5
|
+
// deliberately rides the official `langfuse` SDK's own batched ingestion instead
|
|
6
|
+
// of the OTel pipeline so prompt-bearing telemetry can never reach the Axiom
|
|
7
|
+
// OTLP exporters by construction.
|
|
8
|
+
//
|
|
9
|
+
// Fail-open doctrine: tracing must never fail or stall product work. Every
|
|
10
|
+
// method swallows internally (worst case: one throttled metadata-only warn via
|
|
11
|
+
// log()), `flush()`/`shutdown()` never reject and are time-bounded, and a
|
|
12
|
+
// missing flag/key yields `null` (callers no-op). Deterministic observation ids
|
|
13
|
+
// are the caller's job — Langfuse upserts by id, which is what makes worker
|
|
14
|
+
// lease-reclaim replays converge instead of duplicating.
|
|
15
|
+
import { Langfuse } from "langfuse";
|
|
16
|
+
import { log } from "./log.js";
|
|
17
|
+
const FLUSH_TIMEOUT_MS = 5_000;
|
|
18
|
+
const WARN_THROTTLE_MS = 30_000;
|
|
19
|
+
// Defensive per-field bound, well under Langfuse's ~1 MB event cap. Copilot
|
|
20
|
+
// transcripts max out around 150 KB; anything larger is truncated with an
|
|
21
|
+
// explicit marker rather than risking a rejected ingestion batch.
|
|
22
|
+
const MAX_JSON_FIELD_CHARS = 400_000;
|
|
23
|
+
/**
|
|
24
|
+
* FAIL CLOSED: LLM tracing is on only when OXYGEN_LLM_TRACING_ENABLED is exactly
|
|
25
|
+
* "1"/"true" (trimmed, case-insensitive) AND both Langfuse keys are present.
|
|
26
|
+
*/
|
|
27
|
+
export function isLlmTracingEnabled(env = process.env) {
|
|
28
|
+
const raw = env.OXYGEN_LLM_TRACING_ENABLED;
|
|
29
|
+
if (typeof raw !== "string")
|
|
30
|
+
return false;
|
|
31
|
+
const normalized = raw.trim().toLowerCase();
|
|
32
|
+
if (normalized !== "1" && normalized !== "true")
|
|
33
|
+
return false;
|
|
34
|
+
return ((env.LANGFUSE_PUBLIC_KEY?.trim() ?? "") !== "" &&
|
|
35
|
+
(env.LANGFUSE_SECRET_KEY?.trim() ?? "") !== "");
|
|
36
|
+
}
|
|
37
|
+
// The Langfuse "environment" dimension separates dev/prod traces inside one
|
|
38
|
+
// project. Explicit LANGFUSE_TRACING_ENVIRONMENT (Doppler, per config) wins.
|
|
39
|
+
// Defaults per runtime: web has VERCEL_ENV; the Fly worker has NO VERCEL_ENV
|
|
40
|
+
// (and NODE_ENV=production on BOTH worker apps), so mirror the worker telemetry
|
|
41
|
+
// convention (apps/worker/src/telemetry.ts): the Fly-injected app name is the
|
|
42
|
+
// ground truth for prod vs dev — the "-dev" suffix marks the dev app; a
|
|
43
|
+
// cross-env Doppler drift cannot fake it (the OXY-1183 lesson) — then
|
|
44
|
+
// FLY_ENVIRONMENT as the softer signal. Everything else → development.
|
|
45
|
+
export function resolveLlmTracingEnvironment(env = process.env) {
|
|
46
|
+
const explicit = env.LANGFUSE_TRACING_ENVIRONMENT?.trim().toLowerCase();
|
|
47
|
+
if (explicit)
|
|
48
|
+
return explicit.replace(/[^a-z0-9_-]/g, "-");
|
|
49
|
+
if (env.VERCEL_ENV)
|
|
50
|
+
return env.VERCEL_ENV === "production" ? "production" : "development";
|
|
51
|
+
const flyApp = env.FLY_APP_NAME ?? env.FLY_APP;
|
|
52
|
+
if (flyApp)
|
|
53
|
+
return flyApp.includes("-dev") ? "development" : "production";
|
|
54
|
+
const flyEnv = env.FLY_ENVIRONMENT?.trim().toLowerCase();
|
|
55
|
+
if (flyEnv)
|
|
56
|
+
return flyEnv === "production" ? "production" : "development";
|
|
57
|
+
return "development";
|
|
58
|
+
}
|
|
59
|
+
// Bound one JSON-bearing field. Over the cap → an explicit truncation marker
|
|
60
|
+
// (never a silently clipped payload that parses as complete).
|
|
61
|
+
function boundJsonField(value) {
|
|
62
|
+
if (value === undefined || value === null)
|
|
63
|
+
return value;
|
|
64
|
+
let serialized;
|
|
65
|
+
try {
|
|
66
|
+
serialized = JSON.stringify(value) ?? "";
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return { truncated: true, reason: "unserializable" };
|
|
70
|
+
}
|
|
71
|
+
if (serialized.length <= MAX_JSON_FIELD_CHARS)
|
|
72
|
+
return value;
|
|
73
|
+
return {
|
|
74
|
+
truncated: true,
|
|
75
|
+
chars: serialized.length,
|
|
76
|
+
preview: serialized.slice(0, MAX_JSON_FIELD_CHARS),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function compact(body) {
|
|
80
|
+
const out = {};
|
|
81
|
+
for (const [key, value] of Object.entries(body)) {
|
|
82
|
+
if (value === undefined)
|
|
83
|
+
continue;
|
|
84
|
+
out[key] = key === "input" || key === "output" ? boundJsonField(value) : value;
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
}
|
|
88
|
+
function boundedNever(rejectable, warn) {
|
|
89
|
+
return new Promise((resolve) => {
|
|
90
|
+
const timer = setTimeout(resolve, FLUSH_TIMEOUT_MS);
|
|
91
|
+
timer.unref?.();
|
|
92
|
+
rejectable
|
|
93
|
+
.catch((error) => warn(error))
|
|
94
|
+
.finally(() => {
|
|
95
|
+
clearTimeout(timer);
|
|
96
|
+
resolve();
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Construct a fail-open Langfuse client, or `null` when tracing is disabled or
|
|
102
|
+
* misconfigured. Prefer the process-wide `getLlmTracingClient` in app code;
|
|
103
|
+
* this direct factory exists for tests (inject `langfuseImpl`).
|
|
104
|
+
*/
|
|
105
|
+
export function createLlmTracingClient(env = process.env, options) {
|
|
106
|
+
if (!options?.langfuseImpl && !isLlmTracingEnabled(env))
|
|
107
|
+
return null;
|
|
108
|
+
let lastWarnAtMs = 0;
|
|
109
|
+
const warn = (error, context) => {
|
|
110
|
+
const nowMs = Date.now();
|
|
111
|
+
if (nowMs - lastWarnAtMs < WARN_THROTTLE_MS)
|
|
112
|
+
return;
|
|
113
|
+
lastWarnAtMs = nowMs;
|
|
114
|
+
log("warn", "llm_tracing.ingest_failed", {
|
|
115
|
+
provider: "langfuse",
|
|
116
|
+
error_message: error instanceof Error ? error.message : String(error),
|
|
117
|
+
...context,
|
|
118
|
+
});
|
|
119
|
+
};
|
|
120
|
+
let sdk;
|
|
121
|
+
try {
|
|
122
|
+
sdk =
|
|
123
|
+
options?.langfuseImpl ??
|
|
124
|
+
new Langfuse({
|
|
125
|
+
publicKey: env.LANGFUSE_PUBLIC_KEY,
|
|
126
|
+
secretKey: env.LANGFUSE_SECRET_KEY,
|
|
127
|
+
...(env.LANGFUSE_BASE_URL?.trim() ? { baseUrl: env.LANGFUSE_BASE_URL.trim() } : {}),
|
|
128
|
+
environment: resolveLlmTracingEnvironment(env),
|
|
129
|
+
sdkIntegration: "oxygen",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
catch (error) {
|
|
133
|
+
warn(error, { stage: "construct" });
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
// The SDK surfaces async ingest failures on its emitter; unheard, they are
|
|
137
|
+
// unhandled rejections. Route them into the throttled warn.
|
|
138
|
+
try {
|
|
139
|
+
sdk.on?.("error", (error) => warn(error, { stage: "ingest" }));
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// an emitter-less test double is fine
|
|
143
|
+
}
|
|
144
|
+
const guarded = (fn, stage) => {
|
|
145
|
+
try {
|
|
146
|
+
fn();
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
warn(error, { stage });
|
|
150
|
+
}
|
|
151
|
+
};
|
|
152
|
+
return {
|
|
153
|
+
trace: (body) => guarded(() => void sdk.trace(compact(body)), "trace"),
|
|
154
|
+
span: (body) => guarded(() => void sdk.span(compact(body)), "span"),
|
|
155
|
+
generation: (body) => guarded(() => void sdk.generation(compact(body)), "generation"),
|
|
156
|
+
event: (body) => guarded(() => void sdk.event(compact(body)), "event"),
|
|
157
|
+
flush: () => boundedNever(sdk.flushAsync(), (error) => warn(error, { stage: "flush" })),
|
|
158
|
+
shutdown: () => boundedNever(sdk.shutdownAsync(), (error) => warn(error, { stage: "shutdown" })),
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
// --- Process-wide singleton (both runtimes construct at most one client) ------
|
|
162
|
+
let cached = null;
|
|
163
|
+
function cacheKey(env) {
|
|
164
|
+
return [
|
|
165
|
+
env.OXYGEN_LLM_TRACING_ENABLED ?? "",
|
|
166
|
+
env.LANGFUSE_PUBLIC_KEY ?? "",
|
|
167
|
+
env.LANGFUSE_SECRET_KEY ?? "",
|
|
168
|
+
env.LANGFUSE_BASE_URL ?? "",
|
|
169
|
+
resolveLlmTracingEnvironment(env),
|
|
170
|
+
].join("|");
|
|
171
|
+
}
|
|
172
|
+
export function getLlmTracingClient(env = process.env) {
|
|
173
|
+
const key = cacheKey(env);
|
|
174
|
+
if (cached && cached.key === key)
|
|
175
|
+
return cached.client;
|
|
176
|
+
cached = { key, client: createLlmTracingClient(env) };
|
|
177
|
+
return cached.client;
|
|
178
|
+
}
|
|
179
|
+
/** Flush the singleton if it exists. Never rejects. Hang off request/cycle ends. */
|
|
180
|
+
export function flushLlmTracing(env = process.env) {
|
|
181
|
+
if (!isLlmTracingEnabled(env))
|
|
182
|
+
return Promise.resolve();
|
|
183
|
+
const client = getLlmTracingClient(env);
|
|
184
|
+
return client ? client.flush() : Promise.resolve();
|
|
185
|
+
}
|
|
186
|
+
// Serverless flush scheduling (the axiom-log-shipper pattern): the web runtime
|
|
187
|
+
// registers next/server's after() here at instrumentation time; emission sites
|
|
188
|
+
// then call scheduleLlmTracingFlush() and the flush runs post-response instead
|
|
189
|
+
// of adding latency inside the request. Off-web (worker, tests) there is no
|
|
190
|
+
// scheduler and the flush degrades to fire-and-forget — the worker's cycle-end
|
|
191
|
+
// awaited flush + SDK interval flush are the durability guarantee there.
|
|
192
|
+
//
|
|
193
|
+
// The registration lives on globalThis, not in a module-local: Next.js gives
|
|
194
|
+
// instrumentation.ts and each route handler their own copy of this module, so a
|
|
195
|
+
// module-local set at instrumentation time is invisible to the routes that emit
|
|
196
|
+
// traces — their flushes silently degraded to fire-and-forget, which a Vercel
|
|
197
|
+
// post-response freeze can kill. Same slot pattern as setLogSink and the
|
|
198
|
+
// LinkedIn/WhatsApp quota enforcers. (The `cached` client stays module-local on
|
|
199
|
+
// purpose: each bundle flushing its own client is correct — the run closure
|
|
200
|
+
// below flushes the CALLING bundle's client through the shared scheduler.)
|
|
201
|
+
const FLUSH_SCHEDULER_SLOT = Symbol.for("oxygen.llm_tracing.flush_scheduler");
|
|
202
|
+
export function setLlmTracingFlushScheduler(scheduler) {
|
|
203
|
+
globalThis[FLUSH_SCHEDULER_SLOT] = scheduler;
|
|
204
|
+
}
|
|
205
|
+
/** Request-safe flush trigger: post-response on web, fire-and-forget elsewhere. */
|
|
206
|
+
export function scheduleLlmTracingFlush(env = process.env) {
|
|
207
|
+
if (!isLlmTracingEnabled(env))
|
|
208
|
+
return;
|
|
209
|
+
const run = () => flushLlmTracing(env);
|
|
210
|
+
const flushScheduler = globalThis[FLUSH_SCHEDULER_SLOT] ?? null;
|
|
211
|
+
if (flushScheduler) {
|
|
212
|
+
try {
|
|
213
|
+
flushScheduler(run);
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
// Outside a request scope after() throws — degrade to fire-and-forget.
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
void run();
|
|
221
|
+
}
|
|
222
|
+
/** Shutdown the singleton (worker exit). Never rejects. */
|
|
223
|
+
export function shutdownLlmTracing(env = process.env) {
|
|
224
|
+
const client = cached?.client;
|
|
225
|
+
cached = null;
|
|
226
|
+
return client ? client.shutdown() : Promise.resolve();
|
|
227
|
+
}
|
|
228
|
+
export function __resetLlmTracingForTests() {
|
|
229
|
+
cached = null;
|
|
230
|
+
globalThis[FLUSH_SCHEDULER_SLOT] = null;
|
|
231
|
+
}
|
|
@@ -57,9 +57,21 @@ function reserveSurfaceDimension(merged, ctx) {
|
|
|
57
57
|
surface: ctx.surface,
|
|
58
58
|
};
|
|
59
59
|
}
|
|
60
|
-
|
|
60
|
+
// The sink registration must live on globalThis, not in a module-local variable:
|
|
61
|
+
// Next.js compiles instrumentation.ts and each route handler into separate
|
|
62
|
+
// bundles, each with its OWN instance of this module. A module-local set by the
|
|
63
|
+
// web log shipper (which starts in instrumentation) is invisible to route
|
|
64
|
+
// handlers — exactly why only startup-context lines (the shipper beacon, the
|
|
65
|
+
// framework console filter) ever shipped flat while 4k+ route-emitted
|
|
66
|
+
// error/warn lines/day stayed drain-only (2026-07-21). globalThis is the one
|
|
67
|
+
// object all bundle copies share; Symbol.for gives every copy the same slot.
|
|
68
|
+
// Same trap + same fix as linkedin-quota's enforcer registration.
|
|
69
|
+
const LOG_SINK_SLOT = Symbol.for("oxygen.log.sink");
|
|
61
70
|
export function setLogSink(sink) {
|
|
62
|
-
|
|
71
|
+
globalThis[LOG_SINK_SLOT] = sink;
|
|
72
|
+
}
|
|
73
|
+
function getLogSink() {
|
|
74
|
+
return globalThis[LOG_SINK_SLOT] ?? null;
|
|
63
75
|
}
|
|
64
76
|
export function log(level, msg, fields) {
|
|
65
77
|
const context = getLogContext();
|
|
@@ -89,6 +101,7 @@ export function log(level, msg, fields) {
|
|
|
89
101
|
else {
|
|
90
102
|
console.log(line);
|
|
91
103
|
}
|
|
104
|
+
const logSink = getLogSink();
|
|
92
105
|
if (!logSink)
|
|
93
106
|
return;
|
|
94
107
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** Primitive kinds that participate in the workspace tag union today. */
|
|
2
|
-
export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe"];
|
|
2
|
+
export declare const TAG_KINDS: readonly ["knowledge_page", "publishing_post", "sequence", "table", "workflow", "recipe", "conversation", "mailbox", "sender", "record"];
|
|
3
3
|
export type TagKind = (typeof TAG_KINDS)[number];
|
|
4
4
|
export declare function isTagKind(value: unknown): value is TagKind;
|
|
5
5
|
/** Hard cap per tagged item — matches the wiki's long-standing limit. */
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export const OXYGEN_VERSION = "1.
|
|
1
|
+
export const OXYGEN_VERSION = "1.377.3";
|
|
2
2
|
// The GLOBAL CLI compatibility floor: the oldest CLI allowed to call any
|
|
3
3
|
// operational route. Raising it hard-rejects every older CLI from the entire
|
|
4
4
|
// product, so it obeys one law, enforced by scripts/ci/cli-min-version-gate.mjs:
|
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
/** The
|
|
2
|
-
export type WorkspaceAgentSlug = "inbox-reply-drafts" | "meeting-notetaker" | "knowledge-synthesis";
|
|
1
|
+
/** The implemented general Workspace Agent and specialists (incl. the auto-tagger), by stable slug. */
|
|
2
|
+
export type WorkspaceAgentSlug = "workspace" | "inbox-reply-drafts" | "meeting-notetaker" | "knowledge-synthesis" | "inbox-auto-tagger";
|
|
3
3
|
/**
|
|
4
4
|
* How the web layer builds a specialist's deep-link. A pure hint on the
|
|
5
5
|
* descriptor (shared can't import the web URL helpers) that agent-registry
|
|
6
6
|
* switches on to produce the actual `https://oxygen-agent.com/...` URL.
|
|
7
7
|
*/
|
|
8
|
-
export type WorkspaceAgentDeepLinkKind = "inbox_reply_agent" | "notetaker" | "knowledge_agent";
|
|
8
|
+
export type WorkspaceAgentDeepLinkKind = "workspace_agent" | "inbox_reply_agent" | "notetaker" | "knowledge_agent" | "inbox_auto_tagger";
|
|
9
9
|
/** A pure, serializable descriptor for one Workspace Agent specialist. */
|
|
10
10
|
export type WorkspaceAgentDescriptor = {
|
|
11
11
|
/** Stable identifier used across CLI/MCP/API. Never derived from the title. */
|
|
@@ -54,12 +54,13 @@ export type WorkspaceAgentRunItem = {
|
|
|
54
54
|
webUrl: string | null;
|
|
55
55
|
};
|
|
56
56
|
/**
|
|
57
|
-
* The roster.
|
|
58
|
-
* stable (it drives the default CLI/widget ordering).
|
|
59
|
-
* deliberate product decision that requires the
|
|
57
|
+
* The roster. The general Workspace Agent plus the implemented specialists
|
|
58
|
+
* (five entries). Order is stable (it drives the default CLI/widget ordering).
|
|
59
|
+
* Adding an entry is a deliberate product decision that requires the worker to
|
|
60
|
+
* actually ship first.
|
|
60
61
|
*/
|
|
61
62
|
export declare const WORKSPACE_AGENT_ROSTER: readonly WorkspaceAgentDescriptor[];
|
|
62
|
-
/** True when `value` is one of the
|
|
63
|
+
/** True when `value` is one of the implemented specialist slugs. */
|
|
63
64
|
export declare function isWorkspaceAgentSlug(value: unknown): value is WorkspaceAgentSlug;
|
|
64
65
|
/** The descriptor for `slug`, or `undefined` when it is not a known specialist. */
|
|
65
66
|
export declare function getWorkspaceAgentDescriptor(slug: string): WorkspaceAgentDescriptor | undefined;
|
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
// The Agents primitive's roster (ADR 0011): the governed registry of OXYGEN's
|
|
2
|
-
// built-in workspace agents.
|
|
2
|
+
// built-in workspace agents. The general Workspace Agent is backed by the
|
|
3
|
+
// first-class ox_agents runtime; specialists remain computed from their owning
|
|
4
|
+
// primitive stores. Each
|
|
3
5
|
// entry is a product-defined agent that ships as an approval-gated, observable
|
|
4
6
|
// behavior elsewhere in the product, surfaced under /agents and `oxygen agent`.
|
|
5
7
|
// Agents are governed workspace workers: the runtime envelope (approval posture,
|
|
6
8
|
// credit budgets, run observability) is platform-enforced, never agent-optional.
|
|
7
|
-
// User-defined custom agents are ratified (ADR 0011) but arrive
|
|
8
|
-
//
|
|
9
|
+
// User-defined custom agents are ratified (ADR 0011) but arrive after the unified
|
|
10
|
+
// Workspace Agent runtime — until then there is no custom create/delete verb,
|
|
9
11
|
// and there are never placeholder rows: the roster lists only agents that are
|
|
10
12
|
// actually implemented today. CRM-hygiene, deliverability, and any other roadmap
|
|
11
13
|
// agent stay OUT until they ship — the no-placeholder-surface rule.
|
|
@@ -15,11 +17,24 @@
|
|
|
15
17
|
// (config read/write, run history, deep-links) lives in the web layer's
|
|
16
18
|
// agent-registry, which composes these descriptors with the existing helpers.
|
|
17
19
|
/**
|
|
18
|
-
* The roster.
|
|
19
|
-
* stable (it drives the default CLI/widget ordering).
|
|
20
|
-
* deliberate product decision that requires the
|
|
20
|
+
* The roster. The general Workspace Agent plus the implemented specialists
|
|
21
|
+
* (five entries). Order is stable (it drives the default CLI/widget ordering).
|
|
22
|
+
* Adding an entry is a deliberate product decision that requires the worker to
|
|
23
|
+
* actually ship first.
|
|
21
24
|
*/
|
|
22
25
|
export const WORKSPACE_AGENT_ROSTER = [
|
|
26
|
+
{
|
|
27
|
+
slug: "workspace",
|
|
28
|
+
title: "Workspace Agent",
|
|
29
|
+
subtitle: "Runs organization-grounded goals through OXYGEN's governed capability catalog.",
|
|
30
|
+
description: "Takes a workspace goal, plans the work, and operates tables, CRM, context, workflows, sequences, files, and integrations as one durable Agent run.",
|
|
31
|
+
owningPrimitive: "Agents",
|
|
32
|
+
configCommand: "oxygen agent set workspace",
|
|
33
|
+
configApiPath: "/api/cli/agent/workspace",
|
|
34
|
+
mcpConfigTools: ["oxygen_agent_update", "oxygen_agent_run"],
|
|
35
|
+
approvalPosture: "Uses a versioned standing authorization with optional allow, ask, and deny rules; platform previews, caps, deny rules, and run provenance always apply.",
|
|
36
|
+
deepLinkKind: "workspace_agent",
|
|
37
|
+
},
|
|
23
38
|
{
|
|
24
39
|
slug: "inbox-reply-drafts",
|
|
25
40
|
title: "AI Sales Agent",
|
|
@@ -56,8 +71,20 @@ export const WORKSPACE_AGENT_ROSTER = [
|
|
|
56
71
|
approvalPosture: "Writes working knowledge pages as logged revisions; canonical and pinned pages still require proposal approval.",
|
|
57
72
|
deepLinkKind: "knowledge_agent",
|
|
58
73
|
},
|
|
74
|
+
{
|
|
75
|
+
slug: "inbox-auto-tagger",
|
|
76
|
+
title: "AI Inbox Tagger",
|
|
77
|
+
subtitle: "Auto-tags new inbound conversations from your tag allowlist.",
|
|
78
|
+
description: "Classifies every new inbound email, LinkedIn, and WhatsApp conversation against your workspace tag allowlist and applies the matching tags automatically.",
|
|
79
|
+
owningPrimitive: "Messages",
|
|
80
|
+
configCommand: "oxygen inbox auto-tagger",
|
|
81
|
+
configApiPath: "/api/cli/inbox/auto-tagger",
|
|
82
|
+
mcpConfigTools: ["oxygen_inbox_auto_tagger_get", "oxygen_inbox_auto_tagger_set"],
|
|
83
|
+
approvalPosture: "Auto-applies internal workspace tags only — additive, never re-adds a tag you removed, and needs no approval queue because tags never leave the workspace.",
|
|
84
|
+
deepLinkKind: "inbox_auto_tagger",
|
|
85
|
+
},
|
|
59
86
|
];
|
|
60
|
-
/** True when `value` is one of the
|
|
87
|
+
/** True when `value` is one of the implemented specialist slugs. */
|
|
61
88
|
export function isWorkspaceAgentSlug(value) {
|
|
62
89
|
return (typeof value === "string" &&
|
|
63
90
|
WORKSPACE_AGENT_ROSTER.some((descriptor) => descriptor.slug === value));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@oxygen-agent/cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.377.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
|
@@ -32,9 +32,12 @@
|
|
|
32
32
|
"README.md"
|
|
33
33
|
],
|
|
34
34
|
"dependencies": {
|
|
35
|
+
"@aws-sdk/client-s3": "3.1050.0",
|
|
36
|
+
"@aws-sdk/s3-request-presigner": "3.1050.0",
|
|
35
37
|
"@opentelemetry/api": "1.9.1",
|
|
36
38
|
"commander": "14.0.3",
|
|
37
39
|
"esbuild": "0.27.7",
|
|
40
|
+
"langfuse": "3.38.20",
|
|
38
41
|
"read-excel-file": "6.0.3",
|
|
39
42
|
"typescript": "6.0.3",
|
|
40
43
|
"@oxygen/shared": "0.0.0",
|