@oxygen-agent/cli 1.1014.135 → 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.
Files changed (292) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +27 -19
  3. package/dist/cli-output.d.ts +12 -0
  4. package/dist/cli-output.js +395 -0
  5. package/dist/cli-paths.d.ts +1 -0
  6. package/dist/cli-paths.js +3 -0
  7. package/dist/cli-profile-context.d.ts +3 -0
  8. package/dist/cli-profile-context.js +13 -0
  9. package/dist/command-manifest.d.ts +15 -0
  10. package/dist/command-manifest.js +23 -1
  11. package/dist/command-support.d.ts +438 -0
  12. package/dist/command-support.js +1610 -0
  13. package/dist/commands/action-column.d.ts +3 -0
  14. package/dist/commands/action-column.js +43 -0
  15. package/dist/commands/activation.d.ts +3 -0
  16. package/dist/commands/activation.js +24 -0
  17. package/dist/commands/admin.d.ts +3 -0
  18. package/dist/commands/admin.js +347 -0
  19. package/dist/commands/ads.d.ts +3 -0
  20. package/dist/commands/ads.js +62 -0
  21. package/dist/commands/agent.d.ts +3 -0
  22. package/dist/commands/agent.js +274 -0
  23. package/dist/commands/api-keys.d.ts +3 -0
  24. package/dist/commands/api-keys.js +77 -0
  25. package/dist/commands/approvals.d.ts +3 -0
  26. package/dist/commands/approvals.js +518 -0
  27. package/dist/commands/auth.d.ts +3 -0
  28. package/dist/commands/auth.js +230 -0
  29. package/dist/commands/billing.d.ts +3 -0
  30. package/dist/commands/billing.js +357 -0
  31. package/dist/commands/blueprints.d.ts +3 -0
  32. package/dist/commands/blueprints.js +312 -0
  33. package/dist/commands/budget.d.ts +3 -0
  34. package/dist/commands/budget.js +55 -0
  35. package/dist/commands/callables.d.ts +3 -0
  36. package/dist/commands/callables.js +111 -0
  37. package/dist/commands/capabilities.d.ts +3 -0
  38. package/dist/commands/capabilities.js +44 -0
  39. package/dist/commands/cells.d.ts +3 -0
  40. package/dist/commands/cells.js +94 -0
  41. package/dist/commands/columns.d.ts +3 -0
  42. package/dist/commands/columns.js +1628 -0
  43. package/dist/commands/commands.d.ts +3 -0
  44. package/dist/commands/commands.js +45 -0
  45. package/dist/commands/comments.d.ts +3 -0
  46. package/dist/commands/comments.js +188 -0
  47. package/dist/commands/companies.d.ts +3 -0
  48. package/dist/commands/companies.js +312 -0
  49. package/dist/commands/connections.d.ts +3 -0
  50. package/dist/commands/connections.js +39 -0
  51. package/dist/commands/context.d.ts +3 -0
  52. package/dist/commands/context.js +170 -0
  53. package/dist/commands/copilot.d.ts +3 -0
  54. package/dist/commands/copilot.js +565 -0
  55. package/dist/commands/crm.d.ts +3 -0
  56. package/dist/commands/crm.js +1669 -0
  57. package/dist/commands/custom-integrations.d.ts +3 -0
  58. package/dist/commands/custom-integrations.js +140 -0
  59. package/dist/commands/dashboards.d.ts +3 -0
  60. package/dist/commands/dashboards.js +240 -0
  61. package/dist/commands/db.d.ts +3 -0
  62. package/dist/commands/db.js +190 -0
  63. package/dist/commands/deliverability.d.ts +3 -0
  64. package/dist/commands/deliverability.js +90 -0
  65. package/dist/commands/directory.d.ts +3 -0
  66. package/dist/commands/directory.js +101 -0
  67. package/dist/commands/domains.d.ts +3 -0
  68. package/dist/commands/domains.js +604 -0
  69. package/dist/commands/egress.d.ts +3 -0
  70. package/dist/commands/egress.js +130 -0
  71. package/dist/commands/email.d.ts +3 -0
  72. package/dist/commands/email.js +49 -0
  73. package/dist/commands/engagement.d.ts +3 -0
  74. package/dist/commands/engagement.js +189 -0
  75. package/dist/commands/enrich-column.d.ts +3 -0
  76. package/dist/commands/enrich-column.js +149 -0
  77. package/dist/commands/enrichment.d.ts +3 -0
  78. package/dist/commands/enrichment.js +49 -0
  79. package/dist/commands/feedback.d.ts +3 -0
  80. package/dist/commands/feedback.js +96 -0
  81. package/dist/commands/feeds.d.ts +3 -0
  82. package/dist/commands/feeds.js +486 -0
  83. package/dist/commands/find.d.ts +3 -0
  84. package/dist/commands/find.js +159 -0
  85. package/dist/commands/followers.d.ts +3 -0
  86. package/dist/commands/followers.js +39 -0
  87. package/dist/commands/formulas.d.ts +3 -0
  88. package/dist/commands/formulas.js +43 -0
  89. package/dist/commands/home.d.ts +3 -0
  90. package/dist/commands/home.js +22 -0
  91. package/dist/commands/inbox.d.ts +3 -0
  92. package/dist/commands/inbox.js +779 -0
  93. package/dist/commands/integrations.d.ts +3 -0
  94. package/dist/commands/integrations.js +290 -0
  95. package/dist/commands/knowledge.d.ts +3 -0
  96. package/dist/commands/knowledge.js +874 -0
  97. package/dist/commands/lead-sourcing.d.ts +3 -0
  98. package/dist/commands/lead-sourcing.js +52 -0
  99. package/dist/commands/limits.d.ts +3 -0
  100. package/dist/commands/limits.js +54 -0
  101. package/dist/commands/linkedin.d.ts +3 -0
  102. package/dist/commands/linkedin.js +275 -0
  103. package/dist/commands/login.d.ts +3 -0
  104. package/dist/commands/login.js +347 -0
  105. package/dist/commands/logout.d.ts +3 -0
  106. package/dist/commands/logout.js +12 -0
  107. package/dist/commands/mailboxes.d.ts +3 -0
  108. package/dist/commands/mailboxes.js +1290 -0
  109. package/dist/commands/managed-inboxes.d.ts +3 -0
  110. package/dist/commands/managed-inboxes.js +361 -0
  111. package/dist/commands/messages.d.ts +3 -0
  112. package/dist/commands/messages.js +78 -0
  113. package/dist/commands/notetaker.d.ts +3 -0
  114. package/dist/commands/notetaker.js +79 -0
  115. package/dist/commands/observability.d.ts +3 -0
  116. package/dist/commands/observability.js +125 -0
  117. package/dist/commands/orgs.d.ts +3 -0
  118. package/dist/commands/orgs.js +237 -0
  119. package/dist/commands/people.d.ts +3 -0
  120. package/dist/commands/people.js +353 -0
  121. package/dist/commands/posts.d.ts +3 -0
  122. package/dist/commands/posts.js +139 -0
  123. package/dist/commands/profiles.d.ts +3 -0
  124. package/dist/commands/profiles.js +250 -0
  125. package/dist/commands/projects.d.ts +3 -0
  126. package/dist/commands/projects.js +94 -0
  127. package/dist/commands/prompts.d.ts +3 -0
  128. package/dist/commands/prompts.js +84 -0
  129. package/dist/commands/publishing.d.ts +3 -0
  130. package/dist/commands/publishing.js +1419 -0
  131. package/dist/commands/recipes.d.ts +3 -0
  132. package/dist/commands/recipes.js +120 -0
  133. package/dist/commands/reviews.d.ts +3 -0
  134. package/dist/commands/reviews.js +75 -0
  135. package/dist/commands/rows.d.ts +3 -0
  136. package/dist/commands/rows.js +27 -0
  137. package/dist/commands/runs.d.ts +3 -0
  138. package/dist/commands/runs.js +37 -0
  139. package/dist/commands/search.d.ts +3 -0
  140. package/dist/commands/search.js +182 -0
  141. package/dist/commands/senders.d.ts +3 -0
  142. package/dist/commands/senders.js +393 -0
  143. package/dist/commands/sequences.d.ts +3 -0
  144. package/dist/commands/sequences.js +2322 -0
  145. package/dist/commands/session.d.ts +3 -0
  146. package/dist/commands/session.js +109 -0
  147. package/dist/commands/signals.d.ts +3 -0
  148. package/dist/commands/signals.js +289 -0
  149. package/dist/commands/signup-leads.d.ts +3 -0
  150. package/dist/commands/signup-leads.js +46 -0
  151. package/dist/commands/skills.d.ts +3 -0
  152. package/dist/commands/skills.js +50 -0
  153. package/dist/commands/sourcing.d.ts +3 -0
  154. package/dist/commands/sourcing.js +27 -0
  155. package/dist/commands/status.d.ts +3 -0
  156. package/dist/commands/status.js +51 -0
  157. package/dist/commands/supabase.d.ts +3 -0
  158. package/dist/commands/supabase.js +128 -0
  159. package/dist/commands/support.d.ts +3 -0
  160. package/dist/commands/support.js +651 -0
  161. package/dist/commands/suppressions.d.ts +3 -0
  162. package/dist/commands/suppressions.js +577 -0
  163. package/dist/commands/table-ingestions.d.ts +3 -0
  164. package/dist/commands/table-ingestions.js +145 -0
  165. package/dist/commands/table-runs.d.ts +3 -0
  166. package/dist/commands/table-runs.js +277 -0
  167. package/dist/commands/tables.d.ts +3 -0
  168. package/dist/commands/tables.js +3492 -0
  169. package/dist/commands/tags.d.ts +3 -0
  170. package/dist/commands/tags.js +147 -0
  171. package/dist/commands/tools.d.ts +3 -0
  172. package/dist/commands/tools.js +246 -0
  173. package/dist/commands/update.d.ts +3 -0
  174. package/dist/commands/update.js +106 -0
  175. package/dist/commands/verify.d.ts +3 -0
  176. package/dist/commands/verify.js +27 -0
  177. package/dist/commands/viewers.d.ts +3 -0
  178. package/dist/commands/viewers.js +72 -0
  179. package/dist/commands/voice.d.ts +3 -0
  180. package/dist/commands/voice.js +351 -0
  181. package/dist/commands/whatsapp.d.ts +3 -0
  182. package/dist/commands/whatsapp.js +226 -0
  183. package/dist/commands/whoami.d.ts +3 -0
  184. package/dist/commands/whoami.js +110 -0
  185. package/dist/commands/worker.d.ts +3 -0
  186. package/dist/commands/worker.js +60 -0
  187. package/dist/commands/workflows.d.ts +3 -0
  188. package/dist/commands/workflows.js +1752 -0
  189. package/dist/commands/workspace.d.ts +3 -0
  190. package/dist/commands/workspace.js +94 -0
  191. package/dist/compact-list-flags.d.ts +7 -0
  192. package/dist/compact-list-flags.js +12 -0
  193. package/dist/crm-support-flags.js +14 -5
  194. package/dist/customer-operation-commands.js +8 -6
  195. package/dist/diagnostic-uploader.d.ts +3 -1
  196. package/dist/diagnostic-uploader.js +29 -8
  197. package/dist/help.d.ts +5 -0
  198. package/dist/help.js +22 -0
  199. package/dist/index.d.ts +98 -1
  200. package/dist/index.js +331 -30719
  201. package/dist/local-module-loader.d.ts +4 -0
  202. package/dist/local-module-loader.js +156 -0
  203. package/dist/visual-commands.js +9 -5
  204. package/node_modules/@oxygen/cli-ugc/dist/commands.js +2 -2
  205. package/node_modules/@oxygen/shared/dist/agent-session-schedule.d.ts +36 -0
  206. package/node_modules/@oxygen/shared/dist/agent-session-schedule.js +69 -0
  207. package/node_modules/@oxygen/shared/dist/agent-sessions.d.ts +14 -5
  208. package/node_modules/@oxygen/shared/dist/agent-sessions.js +14 -3
  209. package/node_modules/@oxygen/shared/dist/capability-discovery.js +175 -14
  210. package/node_modules/@oxygen/shared/dist/compact-list-result.d.ts +6 -0
  211. package/node_modules/@oxygen/shared/dist/compact-list-result.js +27 -0
  212. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.d.ts +12 -1
  213. package/node_modules/@oxygen/shared/dist/contracts/agent-sessions.js +5 -2
  214. package/node_modules/@oxygen/shared/dist/contracts/browser-operations.js +4 -2
  215. package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.d.ts +57 -0
  216. package/node_modules/@oxygen/shared/dist/contracts/compact-list-reads.js +23 -0
  217. package/node_modules/@oxygen/shared/dist/contracts/crm-support.d.ts +52 -0
  218. package/node_modules/@oxygen/shared/dist/contracts/crm-support.js +43 -2
  219. package/node_modules/@oxygen/shared/dist/contracts/customer-operations.js +17 -14
  220. package/node_modules/@oxygen/shared/dist/contracts/knowledge-content-reads.js +1 -1
  221. package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.d.ts +49 -0
  222. package/node_modules/@oxygen/shared/dist/contracts/knowledge-proposal-reads.js +14 -0
  223. package/node_modules/@oxygen/shared/dist/contracts/knowledge-sync.js +1 -1
  224. package/node_modules/@oxygen/shared/dist/contracts/limits-read.js +60 -7
  225. package/node_modules/@oxygen/shared/dist/contracts/operations.d.ts +9 -1
  226. package/node_modules/@oxygen/shared/dist/contracts/operations.js +13 -5
  227. package/node_modules/@oxygen/shared/dist/contracts/publishing-reads.js +1 -1
  228. package/node_modules/@oxygen/shared/dist/contracts/teammates.d.ts +16 -0
  229. package/node_modules/@oxygen/shared/dist/contracts/teammates.js +115 -0
  230. package/node_modules/@oxygen/shared/dist/cost-estimate-view.js +21 -3
  231. package/node_modules/@oxygen/shared/dist/cost-estimate.d.ts +31 -1
  232. package/node_modules/@oxygen/shared/dist/cost-estimate.js +56 -15
  233. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.d.ts +2 -0
  234. package/node_modules/@oxygen/shared/dist/diagnostic-credentials.js +2 -0
  235. package/node_modules/@oxygen/shared/dist/diagnostic-lock.js +71 -44
  236. package/node_modules/@oxygen/shared/dist/diagnostic-spool.d.ts +18 -0
  237. package/node_modules/@oxygen/shared/dist/diagnostic-spool.js +348 -0
  238. package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.d.ts +10 -0
  239. package/node_modules/@oxygen/shared/dist/diagnostic-storage-failure.js +42 -0
  240. package/node_modules/@oxygen/shared/dist/diagnostic-storage.d.ts +1 -1
  241. package/node_modules/@oxygen/shared/dist/diagnostic-storage.js +59 -213
  242. package/node_modules/@oxygen/shared/dist/diagnostics.js +3 -2
  243. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +16 -5
  244. package/node_modules/@oxygen/shared/dist/email-dsn.js +97 -1
  245. package/node_modules/@oxygen/shared/dist/emailguard-placement-probe.js +5 -0
  246. package/node_modules/@oxygen/shared/dist/enrichment-intents.js +4 -5
  247. package/node_modules/@oxygen/shared/dist/file-import.js +5 -2
  248. package/node_modules/@oxygen/shared/dist/hosted-ai.d.ts +30 -0
  249. package/node_modules/@oxygen/shared/dist/hosted-ai.js +20 -0
  250. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +6 -2
  251. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +17 -7
  252. package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +81 -17
  253. package/node_modules/@oxygen/shared/dist/langfuse.js +8 -4
  254. package/node_modules/@oxygen/shared/dist/mailbox-egress-ownership.d.ts +1 -1
  255. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +6 -3
  256. package/node_modules/@oxygen/shared/dist/mailbox-import.js +9 -9
  257. package/node_modules/@oxygen/shared/dist/mcp-server-info.d.ts +8 -0
  258. package/node_modules/@oxygen/shared/dist/mcp-server-info.js +10 -0
  259. package/node_modules/@oxygen/shared/dist/object-storage.d.ts +10 -0
  260. package/node_modules/@oxygen/shared/dist/object-storage.js +14 -0
  261. package/node_modules/@oxygen/shared/dist/plan-limits-view.d.ts +5 -0
  262. package/node_modules/@oxygen/shared/dist/plan-limits-view.js +17 -1
  263. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  264. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  265. package/node_modules/@oxygen/shared/dist/provider-selection.d.ts +15 -0
  266. package/node_modules/@oxygen/shared/dist/provider-selection.js +3 -0
  267. package/node_modules/@oxygen/shared/dist/repricing.d.ts +1 -1
  268. package/node_modules/@oxygen/shared/dist/repricing.js +2 -2
  269. package/node_modules/@oxygen/shared/dist/schedule-label.d.ts +6 -0
  270. package/node_modules/@oxygen/shared/dist/schedule-label.js +12 -0
  271. package/node_modules/@oxygen/shared/dist/sequence-failures.d.ts +1 -0
  272. package/node_modules/@oxygen/shared/dist/sequence-failures.js +20 -6
  273. package/node_modules/@oxygen/shared/dist/teammate-groups.d.ts +55 -0
  274. package/node_modules/@oxygen/shared/dist/teammate-groups.js +74 -0
  275. package/node_modules/@oxygen/shared/dist/teammate-messages.d.ts +52 -0
  276. package/node_modules/@oxygen/shared/dist/teammate-messages.js +88 -0
  277. package/node_modules/@oxygen/shared/dist/teammate-notes.d.ts +58 -0
  278. package/node_modules/@oxygen/shared/dist/teammate-notes.js +73 -0
  279. package/node_modules/@oxygen/shared/dist/teammate-routines.d.ts +95 -0
  280. package/node_modules/@oxygen/shared/dist/teammate-routines.js +266 -0
  281. package/node_modules/@oxygen/shared/dist/teammate-tasks.d.ts +56 -0
  282. package/node_modules/@oxygen/shared/dist/teammate-tasks.js +114 -0
  283. package/node_modules/@oxygen/shared/dist/teammates.d.ts +78 -0
  284. package/node_modules/@oxygen/shared/dist/teammates.js +140 -0
  285. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +120 -0
  286. package/node_modules/@oxygen/shared/dist/version.generated.d.ts +1 -1
  287. package/node_modules/@oxygen/shared/dist/version.generated.js +1 -1
  288. package/node_modules/@oxygen/shared/dist/visual-render.d.ts +9 -1
  289. package/node_modules/@oxygen/shared/dist/visual-render.js +12 -5
  290. package/node_modules/@oxygen/shared/dist/warmup-metrics.d.ts +5 -0
  291. package/node_modules/@oxygen/shared/dist/warmup-metrics.js +44 -0
  292. 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
- return `${quantity} × ${credits(line.unit_credits)}${worst}${scheduled}`;
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: "Cheapest plan",
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 ?? expected;
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
- note: enrichment.kind === "estimated"
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 planFit = sheet.comparePlan
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 planText = recommended
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
- `About ${formatInt(credits)} credits a month expected (up to ${formatInt(worstCredits)} worst case)${usdSeats > 0 ? ` plus ${formatUsd(usdSeats)} a month in sending seats` : ""}; ${planText}.`,
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
- for (let attempt = 0; attempt < 2; attempt++) {
6
- // Publish only an initialized owner. A crash between exclusive creation and
7
- // writing a PID must not leave an empty, permanently unknowable lock.
8
- const temporary = path + `.${randomUUID()}.tmp`;
9
- const contents = JSON.stringify({ pid: process.pid, nonce: randomUUID(), start: await processStart(process.pid) });
10
- const handle = await open(temporary, "wx", 0o600).catch(() => null);
11
- if (!handle)
12
- return null;
13
- let published = false;
14
- try {
15
- await handle.writeFile(contents);
16
- await handle.sync();
17
- published = await link(temporary, path).then(() => true, () => false);
18
- }
19
- finally {
20
- await unlink(temporary).catch(() => undefined);
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 alive = true;
23
+ let published = false;
39
24
  try {
40
- process.kill(owner.pid, 0);
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
- catch (error) {
43
- alive = error.code !== "ESRCH";
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 start = alive ? await processStart(owner.pid) : null;
46
- if (alive && (!owner.start || !start || owner.start === start))
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
- if (await readFile(path, "utf8").catch(() => null) === ownerContents)
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>;