@oxygen-agent/cli 1.1014.136 → 1.1014.618
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/admin-primary-providers-render.js +27 -19
- package/dist/cli-output.d.ts +12 -0
- package/dist/cli-output.js +395 -0
- package/dist/cli-paths.d.ts +1 -0
- package/dist/cli-paths.js +3 -0
- package/dist/cli-profile-context.d.ts +3 -0
- package/dist/cli-profile-context.js +13 -0
- package/dist/command-manifest.d.ts +15 -0
- package/dist/command-manifest.js +23 -1
- package/dist/command-support.d.ts +438 -0
- package/dist/command-support.js +1610 -0
- package/dist/commands/action-column.d.ts +3 -0
- package/dist/commands/action-column.js +43 -0
- package/dist/commands/activation.d.ts +3 -0
- package/dist/commands/activation.js +24 -0
- package/dist/commands/admin.d.ts +3 -0
- package/dist/commands/admin.js +347 -0
- package/dist/commands/ads.d.ts +3 -0
- package/dist/commands/ads.js +62 -0
- package/dist/commands/agent.d.ts +3 -0
- package/dist/commands/agent.js +274 -0
- package/dist/commands/api-keys.d.ts +3 -0
- package/dist/commands/api-keys.js +77 -0
- package/dist/commands/approvals.d.ts +3 -0
- package/dist/commands/approvals.js +518 -0
- package/dist/commands/auth.d.ts +3 -0
- package/dist/commands/auth.js +230 -0
- package/dist/commands/billing.d.ts +3 -0
- package/dist/commands/billing.js +357 -0
- package/dist/commands/blueprints.d.ts +3 -0
- package/dist/commands/blueprints.js +312 -0
- package/dist/commands/budget.d.ts +3 -0
- package/dist/commands/budget.js +55 -0
- package/dist/commands/callables.d.ts +3 -0
- package/dist/commands/callables.js +111 -0
- package/dist/commands/capabilities.d.ts +3 -0
- package/dist/commands/capabilities.js +44 -0
- package/dist/commands/cells.d.ts +3 -0
- package/dist/commands/cells.js +94 -0
- package/dist/commands/columns.d.ts +3 -0
- package/dist/commands/columns.js +1628 -0
- package/dist/commands/commands.d.ts +3 -0
- package/dist/commands/commands.js +45 -0
- package/dist/commands/comments.d.ts +3 -0
- package/dist/commands/comments.js +188 -0
- package/dist/commands/companies.d.ts +3 -0
- package/dist/commands/companies.js +312 -0
- package/dist/commands/connections.d.ts +3 -0
- package/dist/commands/connections.js +39 -0
- package/dist/commands/context.d.ts +3 -0
- package/dist/commands/context.js +170 -0
- package/dist/commands/copilot.d.ts +3 -0
- package/dist/commands/copilot.js +565 -0
- package/dist/commands/crm.d.ts +3 -0
- package/dist/commands/crm.js +1669 -0
- package/dist/commands/custom-integrations.d.ts +3 -0
- package/dist/commands/custom-integrations.js +140 -0
- package/dist/commands/dashboards.d.ts +3 -0
- package/dist/commands/dashboards.js +240 -0
- package/dist/commands/db.d.ts +3 -0
- package/dist/commands/db.js +190 -0
- package/dist/commands/deliverability.d.ts +3 -0
- package/dist/commands/deliverability.js +90 -0
- package/dist/commands/directory.d.ts +3 -0
- package/dist/commands/directory.js +101 -0
- package/dist/commands/domains.d.ts +3 -0
- package/dist/commands/domains.js +604 -0
- package/dist/commands/egress.d.ts +3 -0
- package/dist/commands/egress.js +130 -0
- package/dist/commands/email.d.ts +3 -0
- package/dist/commands/email.js +49 -0
- package/dist/commands/engagement.d.ts +3 -0
- package/dist/commands/engagement.js +189 -0
- package/dist/commands/enrich-column.d.ts +3 -0
- package/dist/commands/enrich-column.js +149 -0
- package/dist/commands/enrichment.d.ts +3 -0
- package/dist/commands/enrichment.js +49 -0
- package/dist/commands/feedback.d.ts +3 -0
- package/dist/commands/feedback.js +96 -0
- package/dist/commands/feeds.d.ts +3 -0
- package/dist/commands/feeds.js +486 -0
- package/dist/commands/find.d.ts +3 -0
- package/dist/commands/find.js +159 -0
- package/dist/commands/followers.d.ts +3 -0
- package/dist/commands/followers.js +39 -0
- package/dist/commands/formulas.d.ts +3 -0
- package/dist/commands/formulas.js +43 -0
- package/dist/commands/home.d.ts +3 -0
- package/dist/commands/home.js +22 -0
- package/dist/commands/inbox.d.ts +3 -0
- package/dist/commands/inbox.js +779 -0
- package/dist/commands/integrations.d.ts +3 -0
- package/dist/commands/integrations.js +290 -0
- package/dist/commands/knowledge.d.ts +3 -0
- package/dist/commands/knowledge.js +874 -0
- package/dist/commands/lead-sourcing.d.ts +3 -0
- package/dist/commands/lead-sourcing.js +52 -0
- package/dist/commands/limits.d.ts +3 -0
- package/dist/commands/limits.js +54 -0
- package/dist/commands/linkedin.d.ts +3 -0
- package/dist/commands/linkedin.js +275 -0
- package/dist/commands/login.d.ts +3 -0
- package/dist/commands/login.js +347 -0
- package/dist/commands/logout.d.ts +3 -0
- package/dist/commands/logout.js +12 -0
- package/dist/commands/mailboxes.d.ts +3 -0
- package/dist/commands/mailboxes.js +1290 -0
- package/dist/commands/managed-inboxes.d.ts +3 -0
- package/dist/commands/managed-inboxes.js +361 -0
- package/dist/commands/messages.d.ts +3 -0
- package/dist/commands/messages.js +78 -0
- package/dist/commands/notetaker.d.ts +3 -0
- package/dist/commands/notetaker.js +79 -0
- package/dist/commands/observability.d.ts +3 -0
- package/dist/commands/observability.js +125 -0
- package/dist/commands/orgs.d.ts +3 -0
- package/dist/commands/orgs.js +237 -0
- package/dist/commands/people.d.ts +3 -0
- package/dist/commands/people.js +353 -0
- package/dist/commands/posts.d.ts +3 -0
- package/dist/commands/posts.js +139 -0
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +250 -0
- package/dist/commands/projects.d.ts +3 -0
- package/dist/commands/projects.js +94 -0
- package/dist/commands/prompts.d.ts +3 -0
- package/dist/commands/prompts.js +84 -0
- package/dist/commands/publishing.d.ts +3 -0
- package/dist/commands/publishing.js +1419 -0
- package/dist/commands/recipes.d.ts +3 -0
- package/dist/commands/recipes.js +120 -0
- package/dist/commands/reviews.d.ts +3 -0
- package/dist/commands/reviews.js +75 -0
- package/dist/commands/rows.d.ts +3 -0
- package/dist/commands/rows.js +27 -0
- package/dist/commands/runs.d.ts +3 -0
- package/dist/commands/runs.js +37 -0
- package/dist/commands/search.d.ts +3 -0
- package/dist/commands/search.js +182 -0
- package/dist/commands/senders.d.ts +3 -0
- package/dist/commands/senders.js +393 -0
- package/dist/commands/sequences.d.ts +3 -0
- package/dist/commands/sequences.js +2322 -0
- package/dist/commands/session.d.ts +3 -0
- package/dist/commands/session.js +109 -0
- package/dist/commands/signals.d.ts +3 -0
- package/dist/commands/signals.js +289 -0
- package/dist/commands/signup-leads.d.ts +3 -0
- package/dist/commands/signup-leads.js +46 -0
- package/dist/commands/skills.d.ts +3 -0
- package/dist/commands/skills.js +50 -0
- package/dist/commands/sourcing.d.ts +3 -0
- package/dist/commands/sourcing.js +27 -0
- package/dist/commands/status.d.ts +3 -0
- package/dist/commands/status.js +51 -0
- package/dist/commands/supabase.d.ts +3 -0
- package/dist/commands/supabase.js +128 -0
- package/dist/commands/support.d.ts +3 -0
- package/dist/commands/support.js +651 -0
- package/dist/commands/suppressions.d.ts +3 -0
- package/dist/commands/suppressions.js +577 -0
- package/dist/commands/table-ingestions.d.ts +3 -0
- package/dist/commands/table-ingestions.js +145 -0
- package/dist/commands/table-runs.d.ts +3 -0
- package/dist/commands/table-runs.js +277 -0
- package/dist/commands/tables.d.ts +3 -0
- package/dist/commands/tables.js +3492 -0
- package/dist/commands/tags.d.ts +3 -0
- package/dist/commands/tags.js +147 -0
- package/dist/commands/tools.d.ts +3 -0
- package/dist/commands/tools.js +246 -0
- package/dist/commands/update.d.ts +3 -0
- package/dist/commands/update.js +106 -0
- package/dist/commands/verify.d.ts +3 -0
- package/dist/commands/verify.js +27 -0
- package/dist/commands/viewers.d.ts +3 -0
- package/dist/commands/viewers.js +72 -0
- package/dist/commands/voice.d.ts +3 -0
- package/dist/commands/voice.js +351 -0
- package/dist/commands/whatsapp.d.ts +3 -0
- package/dist/commands/whatsapp.js +226 -0
- package/dist/commands/whoami.d.ts +3 -0
- package/dist/commands/whoami.js +110 -0
- package/dist/commands/worker.d.ts +3 -0
- package/dist/commands/worker.js +60 -0
- package/dist/commands/workflows.d.ts +3 -0
- package/dist/commands/workflows.js +1752 -0
- package/dist/commands/workspace.d.ts +3 -0
- package/dist/commands/workspace.js +94 -0
- package/dist/compact-list-flags.d.ts +7 -0
- package/dist/compact-list-flags.js +12 -0
- package/dist/crm-support-flags.js +14 -5
- package/dist/customer-operation-commands.js +8 -6
- package/dist/diagnostic-uploader.d.ts +3 -1
- package/dist/diagnostic-uploader.js +29 -8
- package/dist/help.d.ts +5 -0
- package/dist/help.js +22 -0
- package/dist/index.d.ts +98 -1
- package/dist/index.js +331 -30719
- package/dist/local-module-loader.d.ts +4 -0
- package/dist/local-module-loader.js +156 -0
- package/dist/visual-commands.js +9 -5
- package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.d.ts +36 -0
- package/node_modules/@oxygen/shared/dist/agent-session-schedule.js +69 -0
- package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +14 -5
- package/node_modules/@oxygen/shared/dist/agent-sessions.js +14 -3
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +175 -14
- package/node_modules/@oxygen/shared/dist/compact-list-result.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/compact-list-result.js +27 -0
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +12 -1
- package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +5 -2
- package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.d.ts +57 -0
- package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.js +23 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -2
- package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +17 -14
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.d.ts +49 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.js +14 -0
- package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +60 -7
- package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/contracts/operations.js +13 -5
- package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -1
- package/node_modules/@oxygen/shared/dist/contracts/teammates.d.ts +16 -0
- package/node_modules/@oxygen/shared/dist/contracts/teammates.js +115 -0
- package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +21 -3
- package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +31 -1
- package/node_modules/@oxygen/shared/dist/cost-estimate.js +56 -15
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +2 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +71 -44
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.d.ts +18 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +348 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.js +42 -0
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +59 -213
- package/node_modules/@oxygen/shared/dist/diagnostics.js +3 -2
- package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +16 -5
- package/node_modules/@oxygen/shared/dist/email-dsn.js +97 -1
- package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +5 -0
- package/node_modules/@oxygen/shared/dist/enrichment-intents.js +4 -5
- package/node_modules/@oxygen/shared/dist/file-import.js +5 -2
- package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +30 -0
- package/node_modules/@oxygen/shared/dist/hosted-ai.js +20 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -2
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +17 -7
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +81 -17
- package/node_modules/@oxygen/shared/dist/langfuse.js +8 -4
- package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +6 -3
- package/node_modules/@oxygen/shared/dist/mailbox-import.js +9 -9
- package/node_modules/@oxygen/shared/dist/mcp-server-info.d.ts +8 -0
- package/node_modules/@oxygen/shared/dist/mcp-server-info.js +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/object-storage.js +14 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/plan-limits-view.js +17 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
- package/node_modules/@oxygen/shared/dist/provider-selection.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/provider-selection.js +3 -0
- package/node_modules/@oxygen/shared/dist/repricing.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/repricing.js +2 -2
- package/node_modules/@oxygen/shared/dist/schedule-label.d.ts +6 -0
- package/node_modules/@oxygen/shared/dist/schedule-label.js +12 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/sequence-failures.js +20 -6
- package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +55 -0
- package/node_modules/@oxygen/shared/dist/teammate-groups.js +74 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +52 -0
- package/node_modules/@oxygen/shared/dist/teammate-messages.js +88 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.d.ts +58 -0
- package/node_modules/@oxygen/shared/dist/teammate-notes.js +73 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.d.ts +95 -0
- package/node_modules/@oxygen/shared/dist/teammate-routines.js +266 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.d.ts +56 -0
- package/node_modules/@oxygen/shared/dist/teammate-tasks.js +114 -0
- package/node_modules/@oxygen/shared/dist/teammates.d.ts +78 -0
- package/node_modules/@oxygen/shared/dist/teammates.js +140 -0
- package/node_modules/@oxygen/shared/dist/user-capability-routing.js +120 -0
- package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
- package/node_modules/@oxygen/shared/dist/visual-render.d.ts +9 -1
- package/node_modules/@oxygen/shared/dist/visual-render.js +12 -5
- package/node_modules/@oxygen/shared/dist/warmup-metrics.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/warmup-metrics.js +44 -0
- package/package.json +1 -1
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { TEAMMATE_GROUP_MAX_MEMBERS, TEAMMATE_GROUP_MIN_MEMBERS, TEAMMATE_GROUP_TITLE_MAX_CHARS } from "../teammate-groups.js";
|
|
2
|
+
import { TEAMMATE_SLUG_PATTERN } from "../teammate-messages.js";
|
|
3
|
+
import { TEAMMATE_TASK_LIMITS } from "../teammate-tasks.js";
|
|
4
|
+
import { NEW_TEAMMATE_NAME, TEAMMATE_AVATAR_COLORS, TEAMMATE_AVATAR_KEYS, TEAMMATE_DESCRIPTION_MAX_CHARS, TEAMMATE_NAME_MAX_CHARS, } from "../teammates.js";
|
|
5
|
+
import { arr, bool, enumOf, int, obj, str } from "./schema.js";
|
|
6
|
+
/**
|
|
7
|
+
* AI teammates (ADR 0032), branded Oxygents: Agents each person talks to in one
|
|
8
|
+
* ongoing private chat, the workspace's first one ("New Bot" until named) and
|
|
9
|
+
* any others added, none with a role (Tim, 2026-10-13). Staff on dev only.
|
|
10
|
+
* The people-facing names say Oxygent (`oxygen oxygents`, `oxygen_oxygents_*`,
|
|
11
|
+
* Tim 2026-10-12); the HTTP family and the code keep "teammates".
|
|
12
|
+
*/
|
|
13
|
+
function teammateOperation(id, verb, method, suffix, summary, fields = {}, required = [], spends = false) {
|
|
14
|
+
const route = `/api/cli/teammates${suffix}`;
|
|
15
|
+
return {
|
|
16
|
+
id, route, method,
|
|
17
|
+
parameters: [...route.matchAll(/\{(\w+)\}/g)].map((match) => match[1]),
|
|
18
|
+
contract: obj(fields, { required }),
|
|
19
|
+
cli: { path: ["oxygents", verb.replace(/_/g, "-")], summary },
|
|
20
|
+
mcp: { name: `oxygen_oxygents_${verb}`, title: `Oxygen: Oxygents ${verb}`, description: summary, pack: "agents" },
|
|
21
|
+
mutates: method !== "GET", spends,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Group chats (phase 7): one person's private chat with two to six Oxygents,
|
|
26
|
+
* the way Grok Bot's are. Their own route family, so a group never shadows a
|
|
27
|
+
* teammate whose slug happens to read like a route word.
|
|
28
|
+
*/
|
|
29
|
+
function teammateGroupOperation(id, verb, method, suffix, summary, fields = {}, required = [], spends = false) {
|
|
30
|
+
const route = `/api/cli/teammate-groups${suffix}`;
|
|
31
|
+
return {
|
|
32
|
+
id, route, method,
|
|
33
|
+
parameters: [...route.matchAll(/\{(\w+)\}/g)].map((match) => match[1]),
|
|
34
|
+
contract: obj(fields, { required }),
|
|
35
|
+
cli: { path: ["oxygents", verb.replace(/_/g, "-")], summary },
|
|
36
|
+
mcp: { name: `oxygen_oxygents_${verb}`, title: `Oxygen: Oxygents ${verb.replace(/_/g, " ")}`, description: summary, pack: "agents" },
|
|
37
|
+
mutates: method !== "GET", spends,
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
const GROUP_MEMBER_SLUGS = (description) => arr(str({ minLength: 2, maxLength: 49, pattern: TEAMMATE_SLUG_PATTERN }), { maxItems: TEAMMATE_GROUP_MAX_MEMBERS, description });
|
|
41
|
+
export const TEAMMATE_PROFILE_FIELDS = {
|
|
42
|
+
name: str({ minLength: 1, maxLength: TEAMMATE_NAME_MAX_CHARS, description: "What everyone calls it." }),
|
|
43
|
+
description: str({
|
|
44
|
+
maxLength: TEAMMATE_DESCRIPTION_MAX_CHARS,
|
|
45
|
+
description: "The job it does, in a sentence or two; other Oxygents hand it work by it. Empty clears it.",
|
|
46
|
+
}),
|
|
47
|
+
avatar_key: enumOf([...TEAMMATE_AVATAR_KEYS], { description: "The shape of its face." }),
|
|
48
|
+
avatar_color: enumOf([...TEAMMATE_AVATAR_COLORS], { description: "The colour of its face." }),
|
|
49
|
+
};
|
|
50
|
+
export const TEAMMATE_OPERATIONS = [
|
|
51
|
+
teammateOperation("teammates.list", "list", "GET", "", "List this workspace's Oxygents, the first one (is_default) on top: name, what each does, its look and your chat with each (working or waiting for you, unread, its latest words). Pinned ones follow the first; hidden ones are marked."),
|
|
52
|
+
// Adding a teammate (phase 7): a custom Agent that can chat at once and run
|
|
53
|
+
// on its own, under a policy that asks before any write, send or spend. As in
|
|
54
|
+
// Grok Bot, it can start as just "New Bot" and learn its job in the chat.
|
|
55
|
+
teammateOperation("teammates.create", "create", "POST", "", `Add an Oxygent to the workspace, named "${NEW_TEAMMATE_NAME}" unless you name it; tell it its job in your chat with it. It can chat at once and run on its own (background tasks, routines), reading freely and asking before anything that writes, sends or spends. The whole workspace sees it; you own it. Free: adding one starts no inference.`, {
|
|
56
|
+
...TEAMMATE_PROFILE_FIELDS,
|
|
57
|
+
slug: str({ minLength: 2, maxLength: 49, pattern: TEAMMATE_SLUG_PATTERN, description: "Its permanent handle in commands and links; made from the name when omitted." }),
|
|
58
|
+
}),
|
|
59
|
+
teammateOperation("teammates.get", "get", "GET", "/{slug}", "Read one Oxygent's profile and your ongoing private chat with it."),
|
|
60
|
+
teammateOperation("teammates.update", "update", "PATCH", "/{slug}", "Edit an Oxygent's name, what it does (description) or its look (avatar_key, avatar_color). Its instructions, tools and versions are unchanged. Free.", TEAMMATE_PROFILE_FIELDS),
|
|
61
|
+
// Copy and delete (Tim, 2026-10-13: "CRUD actions ... through the right
|
|
62
|
+
// click"; "'delete' should be 'full delete'"), as Grok Bot's Bot menu has.
|
|
63
|
+
teammateOperation("teammates.duplicate", "duplicate", "POST", "/{slug}/duplicate", `Copy an Oxygent as a new one you own, named "<name> copy" unless you name it: its job, look, instructions, skills and tool settings, but not its chats, memory or routines. Free.`, {
|
|
64
|
+
name: str({
|
|
65
|
+
minLength: 1, maxLength: TEAMMATE_NAME_MAX_CHARS,
|
|
66
|
+
description: "What everyone calls the copy. Its slug is made from the name, and a deleted Oxygent's slug stays taken, so a copy can get a numbered one (scout-2).",
|
|
67
|
+
}),
|
|
68
|
+
}),
|
|
69
|
+
teammateOperation("teammates.delete", "delete", "POST", "/{slug}/delete", "Delete an Oxygent for good, for everyone: its profile, every chat with it (group chats included), its routines, team memory and runs. What it made in the workspace stays. Its owner or a workspace admin only; never the first Oxygent. Preview first to see what would go, then delete with confirm set to its slug. Its slug stays taken afterwards. Free; cannot be undone.", {
|
|
70
|
+
preview: bool({ description: "Only say what would go: its chats and the people they belong to, group chats, routines, runs, attached files and team memory entries." }),
|
|
71
|
+
confirm: str({ maxLength: 49, description: "Its slug, repeated, to delete it." }),
|
|
72
|
+
}),
|
|
73
|
+
teammateOperation("teammates.chat", "chat", "POST", "/{slug}/chat", "Open your ongoing private chat with an Oxygent, creating it the first time. Starts no inference."),
|
|
74
|
+
teammateOperation("teammates.message", "message", "POST", "/{slug}/messages", "Send a message in your ongoing private chat with an Oxygent. Starts a turn billed from workspace credits; paid or external actions still need approval.", { message: str({ minLength: 1, maxLength: 16000 }) }, ["message"], true),
|
|
75
|
+
// Read state is the reader's own: a person's client may mark it, a runtime
|
|
76
|
+
// acting in a chat never does (platform-denied, and refused at the route).
|
|
77
|
+
teammateOperation("teammates.read", "read", "POST", "/{slug}/read", "Mark your chat with an Oxygent read up to its newest turn (or turn_index), or mark its newest turn unread again. Free.", { turn_index: int({ minimum: 0 }), unread: bool({ description: "On marks its newest turn unread again." }) }),
|
|
78
|
+
// How a person arranges their own roster (phase 7): only their sidebar moves.
|
|
79
|
+
teammateOperation("teammates.pin", "pin", "POST", "/{slug}/pin", "Pin an Oxygent near the top of your own sidebar, or unpin it. Only your sidebar changes. Free.", { pinned: bool({ description: "Off unpins it; on, or left out, pins it." }) }),
|
|
80
|
+
teammateOperation("teammates.hide", "hide", "POST", "/{slug}/hide", "Hide an Oxygent from your own sidebar, or show it again. Its chat is kept; only your sidebar changes. Free. The first Oxygent cannot be hidden.", { hidden: bool({ description: "Off shows it again; on, or left out, hides it." }) }),
|
|
81
|
+
// Grok Bot's per-Bot "Notifications" switch (Tim, 2026-10-14): one person's own.
|
|
82
|
+
teammateOperation("teammates.notify", "notify", "POST", "/{slug}/notify", "Turn your notifications from an Oxygent off, or back on: whether it notifies you when it finishes or waits on you. Only yours change (for an API key, those of the person who made it). Free.", { notify: bool({ description: "Off silences it for you; on, or left out, turns them back on." }) }),
|
|
83
|
+
// Routines (phase 5) are ongoing Agent sessions: set up, paused and resumed
|
|
84
|
+
// through `agent session`; this lists the caller's own with one teammate.
|
|
85
|
+
teammateOperation("teammates.routines", "routines", "GET", "/{slug}/routines", "List your routines with an Oxygent: schedule, status, next run and last report. Manage one with `agent session` (pause, resume)."),
|
|
86
|
+
teammateOperation("teammates.task", "task", "POST", "/{slug}/tasks", "Start a background task from your chat with an Oxygent: an unattended run of it, as you, within max_credits. Its result posts into the chat; it and the run are visible to the workspace. Requires approved:true.", {
|
|
87
|
+
title: str({ minLength: 1, maxLength: TEAMMATE_TASK_LIMITS.titleChars }),
|
|
88
|
+
goal: str({ minLength: 1, maxLength: TEAMMATE_TASK_LIMITS.goalChars, description: "What the run does, without needing the chat." }),
|
|
89
|
+
max_credits: int({ minimum: 1, maximum: TEAMMATE_TASK_LIMITS.maxCredits }),
|
|
90
|
+
approved: bool({ description: "true: you approve this run and its ceiling." }),
|
|
91
|
+
proposal_id: str({ maxLength: 64, description: "The proposal from the chat this starts; starting it twice starts one run." }),
|
|
92
|
+
}, ["title", "goal", "max_credits", "approved"], true),
|
|
93
|
+
// Shared context (phase 4): the team's memory for a teammate (a Knowledge
|
|
94
|
+
// page every chat with it reads), each person's own notes, and its work log.
|
|
95
|
+
teammateOperation("teammates.memory", "memory", "GET", "/{slug}/memory", "List what the team asked an Oxygent to remember. Every chat with it reads this; it is a Knowledge page."),
|
|
96
|
+
teammateOperation("teammates.remember", "remember", "POST", "/{slug}/memory", "Add one thing to an Oxygent's team memory: every chat with it reads it, and the whole workspace can see it. Free.", { text: str({ minLength: 1, maxLength: 500 }) }, ["text"]),
|
|
97
|
+
teammateOperation("teammates.forget", "forget", "DELETE", "/{slug}/memory/{entry_id}", "Remove one entry from an Oxygent's team memory."),
|
|
98
|
+
teammateOperation("teammates.notes", "notes", "GET", "/{slug}/notes", "Read what an Oxygent keeps about you: private to you, read only in your own chat with it."),
|
|
99
|
+
teammateOperation("teammates.notes_set", "notes_set", "POST", "/{slug}/notes", "Replace what an Oxygent keeps about you; empty clears it. Private to you.", { notes: str({ maxLength: 4000 }) }, ["notes"]),
|
|
100
|
+
teammateOperation("teammates.activity", "activity", "GET", "/{slug}/activity", "List an Oxygent's recent work: its runs and the actions taken in its chats, as metadata only (no goals or messages)."),
|
|
101
|
+
teammateGroupOperation("teammate_groups.list", "groups", "GET", "", "List your group chats with Oxygents: name, members and the chat (working or waiting for you, unread, the latest words and who said them). Private to you."),
|
|
102
|
+
teammateGroupOperation("teammate_groups.create", "group_create", "POST", "", `Start a private group chat with ${TEAMMATE_GROUP_MIN_MEMBERS} to ${TEAMMATE_GROUP_MAX_MEMBERS} Oxygents (members, by slug), named name. Starts no inference: @mention a member in a message to have it answer.`, {
|
|
103
|
+
name: str({ minLength: 1, maxLength: TEAMMATE_GROUP_TITLE_MAX_CHARS }),
|
|
104
|
+
members: GROUP_MEMBER_SLUGS("The Oxygents in it, by slug, in the order they show."),
|
|
105
|
+
}, ["name", "members"]),
|
|
106
|
+
teammateGroupOperation("teammate_groups.get", "group", "GET", "/{group_id}", "Read one of your group chats: its name, members and the chat's state."),
|
|
107
|
+
teammateGroupOperation("teammate_groups.update", "group_update", "PATCH", "/{group_id}", `Rename a group chat, or add or remove Oxygents (by slug); it keeps ${TEAMMATE_GROUP_MIN_MEMBERS} to ${TEAMMATE_GROUP_MAX_MEMBERS}. A removed member's earlier messages stay.`, {
|
|
108
|
+
name: str({ minLength: 1, maxLength: TEAMMATE_GROUP_TITLE_MAX_CHARS }),
|
|
109
|
+
add: GROUP_MEMBER_SLUGS("Oxygents to add, by slug."),
|
|
110
|
+
remove: GROUP_MEMBER_SLUGS("Oxygents to remove, by slug."),
|
|
111
|
+
}),
|
|
112
|
+
teammateGroupOperation("teammate_groups.message", "group_message", "POST", "/{group_id}/messages", "Send a message in a group chat. The first Oxygent it @mentions answers (else whoever answered last), then each other one it mentions, or one an answer brings in. Each answer is a turn billed from workspace credits; paid or external actions still need approval.", { message: str({ minLength: 1, maxLength: 16000 }) }, ["message"], true),
|
|
113
|
+
teammateGroupOperation("teammate_groups.read", "group_read", "POST", "/{group_id}/read", "Mark a group chat read up to its newest turn, or up to turn_index.", { turn_index: int({ minimum: 0 }) }),
|
|
114
|
+
teammateGroupOperation("teammate_groups.end", "group_end", "POST", "/{group_id}/end", "End a group chat: nobody answers in it again, and its messages stay readable."),
|
|
115
|
+
];
|
|
@@ -24,15 +24,32 @@ function lineDetail(line) {
|
|
|
24
24
|
const scheduled = line.scheduled_unit_credits !== undefined && line.scheduled_unit_credits !== null
|
|
25
25
|
? `; ${credits(line.scheduled_unit_credits)} from the scheduled date`
|
|
26
26
|
: "";
|
|
27
|
-
|
|
27
|
+
const basis = line.estimate_kind === "floor" ? " minimum" : line.estimate_kind === "ceiling" ? " ceiling" : line.estimate_kind === "typical" ? " estimated average" : "";
|
|
28
|
+
const found = line.found_unit_credits === undefined ? "" : `; ${credits(line.found_unit_credits)} per found result, 0 on a miss`;
|
|
29
|
+
const range = line.found_unit_credits !== undefined && line.minimum_credits_per_month !== undefined && line.worst_case_credits_per_month !== null
|
|
30
|
+
? `; outcome range ${credits(line.minimum_credits_per_month)}–${credits(line.worst_case_credits_per_month)}`
|
|
31
|
+
: "";
|
|
32
|
+
return `${quantity} × ${credits(line.unit_credits)}${basis}${found}${worst}${range}${scheduled}`;
|
|
28
33
|
}
|
|
29
34
|
function lineMonthly(line, worst) {
|
|
30
35
|
if (line.billed_as === "usd")
|
|
31
36
|
return usd(line.usd_per_month);
|
|
32
37
|
return credits(worst ? line.worst_case_credits_per_month : line.credits_per_month);
|
|
33
38
|
}
|
|
39
|
+
function topupText(outcome) {
|
|
40
|
+
return `short ${credits(outcome.shortfall_credits)}, top up ${credits(outcome.topup_credits)} (${usd(outcome.topup_usd)})`;
|
|
41
|
+
}
|
|
34
42
|
function planText(outcome) {
|
|
43
|
+
// A top-up cannot clear a plan wall: sending accounts connect only on a paid plan.
|
|
44
|
+
if (outcome.plan_wall)
|
|
45
|
+
return "Sending accounts need a paid plan; a top-up does not help";
|
|
35
46
|
const total = outcome.monthly_usd === null ? "" : `${usd(outcome.monthly_usd)} a month all in`;
|
|
47
|
+
if (outcome.basis === "spendable_balance") {
|
|
48
|
+
const runway = outcome.months_covered === null ? "zero-credit scenario" : `${outcome.months_covered} months without future grants`;
|
|
49
|
+
if (outcome.workspace_cap_covers === false)
|
|
50
|
+
return `${credits(outcome.spendable_credits)} spendable; workspace cap remaining ${credits(outcome.workspace_cap_remaining)} is too low. Top-ups do not raise that cap.`;
|
|
51
|
+
return `${credits(outcome.spendable_credits)} spendable; ${outcome.covered ? `covers ${runway}; no top-up needed` : topupText(outcome)}`;
|
|
52
|
+
}
|
|
36
53
|
if (outcome.covered)
|
|
37
54
|
return total ? `Covers it: ${total}` : "Covers it";
|
|
38
55
|
const topup = `short ${credits(outcome.shortfall_credits)}, top up ${credits(outcome.topup_credits)} (${usd(outcome.topup_usd)})`;
|
|
@@ -51,7 +68,7 @@ export function costEstimateView(envelope) {
|
|
|
51
68
|
if (envelope.recommended_plan) {
|
|
52
69
|
const { expected, worst_case: worstCase } = envelope.recommended_plan;
|
|
53
70
|
plans.push({
|
|
54
|
-
label: "
|
|
71
|
+
label: "Monthly allowance comparison",
|
|
55
72
|
expected: `${expected.label}: ${planText(expected)}`,
|
|
56
73
|
worstCase: `${worstCase.label}: ${planText(worstCase)}`,
|
|
57
74
|
});
|
|
@@ -68,7 +85,7 @@ export function costEstimateView(envelope) {
|
|
|
68
85
|
})),
|
|
69
86
|
totals: totals
|
|
70
87
|
? [
|
|
71
|
-
{ label: "Credits a month", expected: credits(totals.credits_per_month), worstCase: credits(totals.worst_case_credits_per_month) },
|
|
88
|
+
{ label: envelope.quote_bounds?.expected_complete === false ? "Priced subtotal / floor" : "Credits a month", expected: credits(totals.credits_per_month), worstCase: envelope.quote_bounds?.upper_bound_credits_per_month === null ? "Unknown" : credits(totals.worst_case_credits_per_month) },
|
|
72
89
|
...(totals.usd_seats_per_month > 0
|
|
73
90
|
? [{ label: "Sending seats (card)", expected: usd(totals.usd_seats_per_month), worstCase: usd(totals.usd_seats_per_month) }]
|
|
74
91
|
: []),
|
|
@@ -84,6 +101,7 @@ export function costEstimateView(envelope) {
|
|
|
84
101
|
})),
|
|
85
102
|
notes: [
|
|
86
103
|
...(envelope.warnings ?? []),
|
|
104
|
+
...(envelope.lines ?? []).flatMap((line) => line.note ? [line.note] : []),
|
|
87
105
|
...(envelope.pricing_schedule?.note ? [envelope.pricing_schedule.note] : []),
|
|
88
106
|
],
|
|
89
107
|
};
|
|
@@ -47,6 +47,10 @@ export type CostEstimateEnrichmentPrice = {
|
|
|
47
47
|
expectedCreditsPerRow: number | null;
|
|
48
48
|
worstCaseCreditsPerRow: number | null;
|
|
49
49
|
hitRatesAssumed: boolean;
|
|
50
|
+
note?: string;
|
|
51
|
+
estimateKind?: "typical" | "ceiling";
|
|
52
|
+
minimumCreditsPerRow?: number;
|
|
53
|
+
foundCreditsPerRow?: number;
|
|
50
54
|
};
|
|
51
55
|
export type CostEstimateSenderPrice = {
|
|
52
56
|
billedAs: "credits";
|
|
@@ -76,6 +80,11 @@ export type CostEstimatePriceSheet = {
|
|
|
76
80
|
rungs: readonly CostEstimatePlanRung[];
|
|
77
81
|
/** The plan the estimate is checked against (the workspace's own by default). */
|
|
78
82
|
comparePlan: CostEstimatePlanRung | null;
|
|
83
|
+
/** Extra recurring resources and static Workflow floors, resolved server-side. */
|
|
84
|
+
additionalLines?: readonly CostEstimateLine[];
|
|
85
|
+
/** A non-reconciling billing-owner snapshot; absent for hypothetical plan comparisons. */
|
|
86
|
+
spendableCredits?: number;
|
|
87
|
+
workspaceCapRemaining?: number | null;
|
|
79
88
|
creditsPerUsd: number;
|
|
80
89
|
topupUsdCentsPer100: number;
|
|
81
90
|
topupMinCredits: number;
|
|
@@ -95,7 +104,7 @@ export type CostEstimatePriceSheet = {
|
|
|
95
104
|
};
|
|
96
105
|
export type CostEstimateLine = {
|
|
97
106
|
key: string;
|
|
98
|
-
group: "enrichment" | "senders" | "sending";
|
|
107
|
+
group: "enrichment" | "senders" | "sending" | "workflow" | "placement";
|
|
99
108
|
label: string;
|
|
100
109
|
quantity: number;
|
|
101
110
|
unit: string;
|
|
@@ -109,6 +118,9 @@ export type CostEstimateLine = {
|
|
|
109
118
|
usd_per_month: number | null;
|
|
110
119
|
scheduled_unit_credits?: number | null;
|
|
111
120
|
note?: string;
|
|
121
|
+
estimate_kind?: "fixed" | "typical" | "ceiling" | "floor" | "unknown";
|
|
122
|
+
minimum_credits_per_month?: number;
|
|
123
|
+
found_unit_credits?: number;
|
|
112
124
|
};
|
|
113
125
|
export type CostEstimatePlanOutcome = {
|
|
114
126
|
plan_key: string;
|
|
@@ -116,12 +128,26 @@ export type CostEstimatePlanOutcome = {
|
|
|
116
128
|
monthly_price_usd: number | null;
|
|
117
129
|
monthly_credits: number;
|
|
118
130
|
covered: boolean;
|
|
131
|
+
basis?: "spendable_balance" | "monthly_allowance";
|
|
132
|
+
spendable_credits?: number;
|
|
133
|
+
workspace_cap_remaining?: number | null;
|
|
134
|
+
workspace_cap_covers?: boolean;
|
|
135
|
+
/** Balance-only runway; null for zero cost, not a promise of future grants. */
|
|
136
|
+
months_covered?: number | null;
|
|
119
137
|
/** Credits the plan does not cover, before rounding to a purchasable top-up. */
|
|
120
138
|
shortfall_credits: number;
|
|
121
139
|
topup_credits: number;
|
|
122
140
|
topup_usd: number;
|
|
123
141
|
/** Plan price + top-ups + seats billed in dollars. */
|
|
124
142
|
monthly_usd: number | null;
|
|
143
|
+
/**
|
|
144
|
+
* The scenario needs something this plan cannot connect, whatever the
|
|
145
|
+
* balance: every sending account connects only on a paid plan (founder,
|
|
146
|
+
* 2026-10-12), `linkedin_connect` for a LinkedIn account and
|
|
147
|
+
* `sender_connect` for a mailbox. `covered` is then false and a top-up does
|
|
148
|
+
* not help.
|
|
149
|
+
*/
|
|
150
|
+
plan_wall?: "linkedin_connect" | "sender_connect";
|
|
125
151
|
};
|
|
126
152
|
export type CostEstimateLimit = {
|
|
127
153
|
key: "email_per_mailbox" | "linkedin_invites_per_account" | "linkedin_messages_per_account";
|
|
@@ -154,6 +180,10 @@ export type CostEstimateResult = {
|
|
|
154
180
|
usd_seats_per_month: number;
|
|
155
181
|
unpriced_lines: string[];
|
|
156
182
|
};
|
|
183
|
+
quote_bounds?: {
|
|
184
|
+
expected_complete: boolean;
|
|
185
|
+
upper_bound_credits_per_month: number | null;
|
|
186
|
+
};
|
|
157
187
|
plan_fit: {
|
|
158
188
|
expected: CostEstimatePlanOutcome;
|
|
159
189
|
worst_case: CostEstimatePlanOutcome;
|
|
@@ -27,6 +27,8 @@ export const COST_ESTIMATE_BOUNDS = {
|
|
|
27
27
|
maxSendersPerKind: 1_000,
|
|
28
28
|
maxEnrichments: 12,
|
|
29
29
|
};
|
|
30
|
+
/** Extra estimate lines that are sending accounts, each walled on Free like a mailbox. */
|
|
31
|
+
const EXTRA_SENDING_ACCOUNT_LINES = new Set(["senders.x_account", "senders.whatsapp_account"]);
|
|
30
32
|
function round2(value) {
|
|
31
33
|
return Math.round((value + Number.EPSILON) * 100) / 100;
|
|
32
34
|
}
|
|
@@ -79,8 +81,8 @@ function senderLine(input) {
|
|
|
79
81
|
: `${formatInt(unit)} credits per ${input.unit} a month, reserved from your balance on the day it connects. ${input.note}`,
|
|
80
82
|
};
|
|
81
83
|
}
|
|
82
|
-
function planOutcome(rung, credits, usdSeats, sheet) {
|
|
83
|
-
const shortfall = Math.max(0, Math.ceil(credits - rung.monthlyCredits));
|
|
84
|
+
function planOutcome(rung, credits, usdSeats, sheet, spendableCredits, planWall) {
|
|
85
|
+
const shortfall = Math.max(0, Math.ceil(credits - (spendableCredits ?? rung.monthlyCredits)));
|
|
84
86
|
// The amount `billing topup` actually sells: whole 100-credit steps, never
|
|
85
87
|
// below the smallest pack. Above 100,000 it takes more than one checkout.
|
|
86
88
|
const topupCredits = shortfall === 0
|
|
@@ -93,11 +95,19 @@ function planOutcome(rung, credits, usdSeats, sheet) {
|
|
|
93
95
|
label: rung.label,
|
|
94
96
|
monthly_price_usd: price,
|
|
95
97
|
monthly_credits: rung.monthlyCredits,
|
|
96
|
-
covered: shortfall === 0,
|
|
98
|
+
covered: !planWall && shortfall === 0 && (spendableCredits === undefined || sheet.workspaceCapRemaining == null || credits <= sheet.workspaceCapRemaining),
|
|
99
|
+
basis: spendableCredits === undefined ? "monthly_allowance" : "spendable_balance",
|
|
100
|
+
...(spendableCredits === undefined ? {} : {
|
|
101
|
+
spendable_credits: spendableCredits,
|
|
102
|
+
workspace_cap_remaining: sheet.workspaceCapRemaining ?? null,
|
|
103
|
+
workspace_cap_covers: sheet.workspaceCapRemaining == null || credits <= sheet.workspaceCapRemaining,
|
|
104
|
+
months_covered: credits === 0 ? null : Math.floor(spendableCredits / credits),
|
|
105
|
+
}),
|
|
97
106
|
shortfall_credits: shortfall,
|
|
98
107
|
topup_credits: topupCredits,
|
|
99
108
|
topup_usd: topupUsd,
|
|
100
109
|
monthly_usd: price === null ? null : round2(price + topupUsd + usdSeats),
|
|
110
|
+
...(planWall ? { plan_wall: planWall } : {}),
|
|
101
111
|
};
|
|
102
112
|
}
|
|
103
113
|
/** The cheapest monthly bill: plan plus top-ups, the smallest plan that covers winning a tie. */
|
|
@@ -174,7 +184,7 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
174
184
|
const warnings = [];
|
|
175
185
|
for (const enrichment of sheet.enrichments) {
|
|
176
186
|
const expected = enrichment.expectedCreditsPerRow;
|
|
177
|
-
const worst = enrichment.worstCaseCreditsPerRow
|
|
187
|
+
const worst = enrichment.worstCaseCreditsPerRow;
|
|
178
188
|
const credits = expected === null ? null : round2(leads * expected);
|
|
179
189
|
const worstCredits = worst === null ? null : round2(leads * worst);
|
|
180
190
|
if (enrichment.kind === "unavailable" || expected === null) {
|
|
@@ -192,11 +202,14 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
192
202
|
credits_per_month: credits,
|
|
193
203
|
worst_case_credits_per_month: worstCredits,
|
|
194
204
|
usd_per_month: credits === null ? null : round2(credits / sheet.creditsPerUsd),
|
|
195
|
-
|
|
205
|
+
estimate_kind: enrichment.estimateKind ?? (enrichment.kind === "fixed" ? "fixed" : enrichment.kind === "estimated" ? "typical" : "unknown"),
|
|
206
|
+
...(enrichment.minimumCreditsPerRow === undefined ? {} : { minimum_credits_per_month: round2(leads * enrichment.minimumCreditsPerRow) }),
|
|
207
|
+
...(enrichment.foundCreditsPerRow === undefined ? {} : { found_unit_credits: enrichment.foundCreditsPerRow }),
|
|
208
|
+
note: enrichment.note ?? (enrichment.kind === "estimated"
|
|
196
209
|
? `Billed only when a provider finds a result${enrichment.hitRatesAssumed ? "; the expected figure uses assumed hit rates" : ""}. Up to ${worst === null ? "an unknown amount" : `${round2(worst)} credits`} a row if the dearest provider answers every row.`
|
|
197
210
|
: enrichment.kind === "fixed"
|
|
198
211
|
? "The same price on every row."
|
|
199
|
-
: "No estimate available.",
|
|
212
|
+
: "No estimate available."),
|
|
200
213
|
});
|
|
201
214
|
}
|
|
202
215
|
const mailboxNote = "Warm-up is included.";
|
|
@@ -256,10 +269,11 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
256
269
|
note: "Sending costs 0 credits; the limits below bound how fast it goes.",
|
|
257
270
|
});
|
|
258
271
|
}
|
|
272
|
+
lines.push(...(sheet.additionalLines ?? []));
|
|
259
273
|
const sum = (pick, filter) => round2(lines.filter(filter).reduce((total, line) => total + (pick(line) ?? 0), 0));
|
|
260
274
|
const fixed = sum((line) => line.credits_per_month, (line) => line.group === "senders" && line.billed_as === "credits");
|
|
261
|
-
const flexible = sum((line) => line.credits_per_month, (line) => line.group === "enrichment");
|
|
262
|
-
const flexibleWorst = sum((line) => line.worst_case_credits_per_month, (line) => line.group === "enrichment");
|
|
275
|
+
const flexible = sum((line) => line.credits_per_month, (line) => line.group === "enrichment" || line.group === "workflow" || line.group === "placement");
|
|
276
|
+
const flexibleWorst = sum((line) => line.worst_case_credits_per_month, (line) => line.group === "enrichment" || line.group === "workflow" || line.group === "placement");
|
|
263
277
|
const usdSeats = sum((line) => line.usd_per_month, (line) => line.billed_as === "usd");
|
|
264
278
|
const unpriced = lines
|
|
265
279
|
.filter((line) => line.billed_as === "credits" && line.credits_per_month === null)
|
|
@@ -271,15 +285,31 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
271
285
|
}
|
|
272
286
|
const credits = round2(fixed + flexible);
|
|
273
287
|
const worstCredits = round2(fixed + flexibleWorst);
|
|
274
|
-
const
|
|
288
|
+
const expectedComplete = unpriced.length === 0 && !lines.some((line) => line.estimate_kind === "floor");
|
|
289
|
+
const upperBound = lines.some((line) => line.billed_as === "credits" && line.worst_case_credits_per_month === null) ? null : worstCredits;
|
|
290
|
+
if (!expectedComplete || upperBound === null)
|
|
291
|
+
warnings.push("Totals are priced subtotals. A floor or unknown cost is not a complete scenario estimate or an approval ceiling; plan recommendations are omitted.");
|
|
292
|
+
// Every sending account connects only on a paid plan (founder, 2026-10-12),
|
|
293
|
+
// so the Free plan cannot carry a scenario with any, however many credits it
|
|
294
|
+
// holds: LinkedIn, mailboxes, and the X and WhatsApp accounts that arrive as
|
|
295
|
+
// extra lines. The LinkedIn wall is named first when several apply.
|
|
296
|
+
const freePlan = sheet.comparePlan?.planKey === "free";
|
|
297
|
+
const extraSendingAccounts = (sheet.additionalLines ?? []).some((line) => EXTRA_SENDING_ACCOUNT_LINES.has(line.key) && line.quantity > 0);
|
|
298
|
+
const planWall = !freePlan ? undefined
|
|
299
|
+
: scenario.linkedinAccounts > 0 ? "linkedin_connect"
|
|
300
|
+
: scenario.mailboxes + scenario.managedMailboxes > 0 || extraSendingAccounts ? "sender_connect"
|
|
301
|
+
: undefined;
|
|
302
|
+
if (planWall)
|
|
303
|
+
warnings.push("Sending accounts connect only on a paid plan; the Free plan cannot connect one, whatever its credits.");
|
|
304
|
+
const planFit = sheet.comparePlan && expectedComplete && upperBound !== null
|
|
275
305
|
? {
|
|
276
|
-
expected: planOutcome(sheet.comparePlan, credits, usdSeats, sheet),
|
|
277
|
-
worst_case: planOutcome(sheet.comparePlan, worstCredits, usdSeats, sheet),
|
|
306
|
+
expected: planOutcome(sheet.comparePlan, credits, usdSeats, sheet, sheet.spendableCredits, planWall),
|
|
307
|
+
worst_case: planOutcome(sheet.comparePlan, worstCredits, usdSeats, sheet, sheet.spendableCredits, planWall),
|
|
278
308
|
}
|
|
279
309
|
: null;
|
|
280
310
|
const recommendedExpected = cheapestRung(credits, usdSeats, sheet);
|
|
281
311
|
const recommendedWorst = cheapestRung(worstCredits, usdSeats, sheet);
|
|
282
|
-
const recommended = recommendedExpected && recommendedWorst
|
|
312
|
+
const recommended = expectedComplete && upperBound !== null && recommendedExpected && recommendedWorst
|
|
283
313
|
? { expected: recommendedExpected, worst_case: recommendedWorst }
|
|
284
314
|
: null;
|
|
285
315
|
const { sending } = sheet;
|
|
@@ -329,6 +359,7 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
329
359
|
const firstMonthRampHit = limits.find((row) => row.key === "email_per_mailbox"
|
|
330
360
|
&& row.first_month_hit && row.first_month_capacity !== null && row.first_month_capacity < row.capacity_per_month);
|
|
331
361
|
const assumptions = [
|
|
362
|
+
"Entered resources are prospective additions. Existing commitments are already excluded from spendable credits; do not enter already-covered resources again. Quotes create no reservations or authority.",
|
|
332
363
|
"Every lead is already in a table and goes through each enrichment and every sequence step. Finding new leads is priced separately.",
|
|
333
364
|
`Sending limits use ${sending.sendingDaysPerMonth} sending days a month and the defaults for a newly connected account (a sender you already run may be set lower): ${sending.emailPerMailboxPerDay} emails a mailbox a day, ${sending.linkedinInvitesPerDay} LinkedIn invites a day and ${sending.linkedinInvitesPerWeek} a week, ${sending.linkedinMessagesPerDay} LinkedIn messages a day.`,
|
|
334
365
|
"The first LinkedIn step is a connection invite and each later step a message to every lead, so the message count is an upper bound: only accepted invites receive messages.",
|
|
@@ -339,14 +370,23 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
339
370
|
const ramp = sending.emailRamp.map((step) => step.perDay).join(", ");
|
|
340
371
|
assumptions.push(`A newly connected mailbox ramps up (${ramp} a day) before it sends at its full daily limit; first_month_capacity, first_month_senders_needed and first_month_hit show that first ${sending.sendingDaysPerMonth}-sending-day window. The other capacity and hit fields describe steady state.`);
|
|
341
372
|
}
|
|
342
|
-
const
|
|
373
|
+
const cheapestText = recommended
|
|
343
374
|
? `cheapest all in: the ${recommended.expected.label} plan${recommended.expected.topup_credits > 0 ? " with top-ups" : ""}${usdSeats > 0 ? " and seats" : ""}, ${formatUsd(recommended.expected.monthly_usd ?? 0)} a month`
|
|
344
|
-
: "no plan could be priced";
|
|
375
|
+
: !expectedComplete || upperBound === null ? "complete plan comparison withheld" : "no plan could be priced";
|
|
376
|
+
const planText = planWall
|
|
377
|
+
? `sending accounts connect only on a paid plan; ${cheapestText}`
|
|
378
|
+
: sheet.spendableCredits !== undefined && planFit?.expected.covered
|
|
379
|
+
? `current spendable credits cover ${planFit.expected.months_covered === null ? "this zero-credit scenario" : `${planFit.expected.months_covered} months of this scenario without future grants`}; no top-up is needed`
|
|
380
|
+
: sheet.spendableCredits !== undefined && planFit?.expected.workspace_cap_covers === false
|
|
381
|
+
? "workspace monthly cap is too low; a top-up does not raise that cap"
|
|
382
|
+
: cheapestText;
|
|
383
|
+
const ceilingOnly = lines.some((line) => line.estimate_kind === "ceiling")
|
|
384
|
+
&& !lines.some((line) => line.estimate_kind === "typical");
|
|
345
385
|
const steps = scenario.emailSteps + scenario.linkedinSteps;
|
|
346
386
|
const scenarioText = `Priced: ${formatInt(leads)} leads a month, ${steps} sequence ${steps === 1 ? "step" : "steps"} per lead (${scenario.emailSteps} email, ${scenario.linkedinSteps} LinkedIn), ${formatInt(totalMailboxes)} ${totalMailboxes === 1 ? "mailbox" : "mailboxes"}, ${formatInt(scenario.linkedinAccounts)} LinkedIn ${scenario.linkedinAccounts === 1 ? "account" : "accounts"}.`;
|
|
347
387
|
const summary = [
|
|
348
388
|
scenarioText,
|
|
349
|
-
|
|
389
|
+
`${expectedComplete ? (ceilingOnly ? "Conservative ceiling" : "About") : "Priced subtotal / floor"}: ${formatInt(credits)} credits a month (${upperBound === null ? "upper bound unknown" : `up to ${formatInt(upperBound)} within the model`})${usdSeats > 0 ? ` plus ${formatUsd(usdSeats)} a month in sending seats` : ""}; ${planText}.`,
|
|
350
390
|
limitsHit.length > 0
|
|
351
391
|
? `Limits hit: ${limits.filter((row) => row.hit).map((row) => `${row.label.toLowerCase()} (needs ${formatInt(row.senders_needed)})`).join(", ")}.`
|
|
352
392
|
: limits.length > 0 ? firstMonthRampHit ? "No steady-state sending limit is hit." : "No sending limit is hit." : "",
|
|
@@ -365,6 +405,7 @@ export function estimateScenarioCost(rawScenario, sheet) {
|
|
|
365
405
|
usd_seats_per_month: usdSeats,
|
|
366
406
|
unpriced_lines: unpriced,
|
|
367
407
|
},
|
|
408
|
+
quote_bounds: { expected_complete: expectedComplete, upper_bound_credits_per_month: upperBound },
|
|
368
409
|
plan_fit: planFit,
|
|
369
410
|
recommended_plan: recommended,
|
|
370
411
|
limits,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
/** Credential-bearing tool contracts already excluded by MCP analytics. */
|
|
2
2
|
export declare const MCP_ARGUMENT_DENYLIST_TOOLS: ReadonlySet<string>;
|
|
3
3
|
export declare const MCP_RESPONSE_DENYLIST_TOOLS: ReadonlySet<string>;
|
|
4
|
+
/** Operations whose output is what a person said aloud (the composer's dictation): never stored. */
|
|
5
|
+
export declare const SPOKEN_OUTPUT_OPERATIONS: ReadonlySet<string>;
|
|
4
6
|
export declare function credentialBearingOperation(operation: string, direction: "input" | "output"): boolean;
|
|
@@ -5,6 +5,8 @@ export const MCP_ARGUMENT_DENYLIST_TOOLS = new Set([
|
|
|
5
5
|
export const MCP_RESPONSE_DENYLIST_TOOLS = new Set([
|
|
6
6
|
"oxygen_senders_connect", "oxygen_whatsapp_connect", "oxygen_mailboxes_connect_oauth", "oxygen_integrations_connect",
|
|
7
7
|
]);
|
|
8
|
+
/** Operations whose output is what a person said aloud (the composer's dictation): never stored. */
|
|
9
|
+
export const SPOKEN_OUTPUT_OPERATIONS = new Set(["copilot dictation"]);
|
|
8
10
|
export function credentialBearingOperation(operation, direction) {
|
|
9
11
|
if (direction === "input")
|
|
10
12
|
return MCP_ARGUMENT_DENYLIST_TOOLS.has(operation) || /(?:senders|linkedin).*checkpoint.*solve|custom.integrations.apply/.test(operation);
|
|
@@ -1,58 +1,85 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { link, open, readFile, unlink } from "node:fs/promises";
|
|
3
|
+
import { acquireDiagnosticStorageLock } from "./diagnostic-storage-lock.js";
|
|
3
4
|
/** Private spool lock. A dead owner is recoverable; age never evicts a live writer. */
|
|
4
5
|
export async function acquireDiagnosticLock(path) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!published)
|
|
22
|
-
await handle.close();
|
|
23
|
-
}
|
|
24
|
-
if (published) {
|
|
25
|
-
return async () => {
|
|
26
|
-
await handle.close();
|
|
27
|
-
if (await readFile(path, "utf8").catch(() => null) === contents)
|
|
28
|
-
await unlink(path).catch(() => undefined);
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
const ownerContents = await readFile(path, "utf8").catch(() => null);
|
|
32
|
-
if (!ownerContents)
|
|
33
|
-
return null; // Unknown/unfinished owner stays protected.
|
|
34
|
-
try {
|
|
35
|
-
const owner = JSON.parse(ownerContents);
|
|
36
|
-
if (!Number.isInteger(owner.pid) || owner.pid <= 0)
|
|
6
|
+
// Comparing an owner and unlinking its pathname is not atomic: two recoverers
|
|
7
|
+
// can both observe a dead owner, then one removes the other's newly live lock.
|
|
8
|
+
// Hold the kernel mutex through recovery, publication and release. The owner
|
|
9
|
+
// file still protects live/unknown clients using the earlier lock protocol.
|
|
10
|
+
const releaseMutex = await acquireDiagnosticStorageLock(path);
|
|
11
|
+
if (!releaseMutex)
|
|
12
|
+
return null;
|
|
13
|
+
let held = false;
|
|
14
|
+
try {
|
|
15
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
16
|
+
// Publish only an initialized owner. A crash between exclusive creation and
|
|
17
|
+
// writing a PID must not leave an empty, permanently unknowable lock.
|
|
18
|
+
const temporary = path + `.${randomUUID()}.tmp`;
|
|
19
|
+
const contents = JSON.stringify({ pid: process.pid, nonce: randomUUID(), start: await processStart(process.pid) });
|
|
20
|
+
const handle = await open(temporary, "wx", 0o600).catch(() => null);
|
|
21
|
+
if (!handle)
|
|
37
22
|
return null;
|
|
38
|
-
let
|
|
23
|
+
let published = false;
|
|
39
24
|
try {
|
|
40
|
-
|
|
25
|
+
await handle.writeFile(contents);
|
|
26
|
+
await handle.sync();
|
|
27
|
+
published = await link(temporary, path).then(() => true, () => false);
|
|
28
|
+
}
|
|
29
|
+
finally {
|
|
30
|
+
await unlink(temporary).catch(() => undefined);
|
|
31
|
+
if (!published)
|
|
32
|
+
await handle.close();
|
|
41
33
|
}
|
|
42
|
-
|
|
43
|
-
|
|
34
|
+
if (published) {
|
|
35
|
+
held = true;
|
|
36
|
+
let closed = false;
|
|
37
|
+
return async () => {
|
|
38
|
+
if (closed)
|
|
39
|
+
return;
|
|
40
|
+
closed = true;
|
|
41
|
+
try {
|
|
42
|
+
await handle.close();
|
|
43
|
+
if (await readFile(path, "utf8").catch(() => null) === contents)
|
|
44
|
+
await unlink(path).catch(() => undefined);
|
|
45
|
+
}
|
|
46
|
+
finally {
|
|
47
|
+
await releaseMutex();
|
|
48
|
+
}
|
|
49
|
+
};
|
|
44
50
|
}
|
|
45
|
-
const
|
|
46
|
-
if (
|
|
51
|
+
const ownerContents = await readFile(path, "utf8").catch(() => null);
|
|
52
|
+
if (!ownerContents)
|
|
53
|
+
return null; // Unknown/unfinished owner stays protected.
|
|
54
|
+
try {
|
|
55
|
+
const owner = JSON.parse(ownerContents);
|
|
56
|
+
if (!Number.isInteger(owner.pid) || owner.pid <= 0)
|
|
57
|
+
return null;
|
|
58
|
+
if (owner.start !== undefined && owner.start !== null && typeof owner.start !== "string")
|
|
59
|
+
return null;
|
|
60
|
+
let alive = true;
|
|
61
|
+
try {
|
|
62
|
+
process.kill(owner.pid, 0);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
alive = error.code !== "ESRCH";
|
|
66
|
+
}
|
|
67
|
+
const start = alive ? await processStart(owner.pid) : null;
|
|
68
|
+
if (alive && (!owner.start || !start || owner.start === start))
|
|
69
|
+
return null;
|
|
70
|
+
if (await readFile(path, "utf8").catch(() => null) === ownerContents)
|
|
71
|
+
await unlink(path).catch(() => undefined);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
47
74
|
return null;
|
|
48
|
-
|
|
49
|
-
await unlink(path).catch(() => undefined);
|
|
50
|
-
}
|
|
51
|
-
catch {
|
|
52
|
-
return null;
|
|
75
|
+
}
|
|
53
76
|
}
|
|
77
|
+
return null;
|
|
78
|
+
}
|
|
79
|
+
finally {
|
|
80
|
+
if (!held)
|
|
81
|
+
await releaseMutex();
|
|
54
82
|
}
|
|
55
|
-
return null;
|
|
56
83
|
}
|
|
57
84
|
async function processStart(pid) {
|
|
58
85
|
const value = await readFile(`/proc/${pid}/stat`, "utf8").catch(() => null);
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DiagnosticRecord } from "./diagnostics.js";
|
|
2
|
+
export type UploadReservation = {
|
|
3
|
+
path: string;
|
|
4
|
+
};
|
|
5
|
+
export type UploadProgress = {
|
|
6
|
+
sha256: string;
|
|
7
|
+
store: string;
|
|
8
|
+
published: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** Admit durable content plus bounded receipt bytes; acknowledge only after publication. */
|
|
11
|
+
export declare function admitDiagnosticSpool(directory: string, record: DiagnosticRecord, compressed: Buffer): Promise<() => Promise<void>>;
|
|
12
|
+
/** Receipt writes share the admission owner's protected byte count and retry reservation. */
|
|
13
|
+
export declare function saveDiagnosticUploadProgress(directory: string, record: DiagnosticRecord, progress: UploadProgress, reservation?: UploadReservation): Promise<void>;
|
|
14
|
+
/** A fixed set of stripes bounds SQLite lock files while coordinating direct and retry owners. */
|
|
15
|
+
export declare function lockDiagnosticUpload(directory: string, id: string, waitMs?: number): Promise<() => Promise<void>>;
|
|
16
|
+
export declare function readUploadProgress(directory: string, id: string, storeFingerprint: string): Promise<UploadProgress | null>;
|
|
17
|
+
/** Replay retained bytes under their stripe; publication owns the upload acknowledgment. */
|
|
18
|
+
export declare function retryDiagnosticSpool(directory: string, storeFingerprint: string, upload: (compressed: Buffer, reservation?: UploadReservation) => Promise<void>): Promise<void>;
|