@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,266 @@
1
+ // AI teammates (ADR 0032): routines.
2
+ //
3
+ // A routine is something a teammate does for one person on a schedule ("every
4
+ // weekday at 9, look at the campaign and tell me what changed"), held as an
5
+ // ongoing Agent session that belongs to the teammate and to the person who set
6
+ // it up from their chat with it: its settings carry `teammate_routine`, the
7
+ // chat its reports post into. Each wake is an unattended run as that person,
8
+ // within the caps they approved. It posts into the chat only when it has
9
+ // something new to say, and an approval it raises is theirs alone and expires.
10
+ //
11
+ // The teammate proposes one; the proposal ENDS the turn, the way a question
12
+ // card does, and the person sets it up from a card through the ordinary session
13
+ // routes (create, preview, activate), which is their approval of exactly that
14
+ // schedule, scope and caps. Nothing is scheduled on the model's word alone.
15
+ //
16
+ // One owner for the contract, because four readers depend on it: the tool that
17
+ // validates the model's proposal, the session read that projects the pending
18
+ // one, the card that sets it up, and the routes that hold it. The proposal is
19
+ // kept in its wire shape (the tool's own input, plus an id), so what the tool
20
+ // stores is exactly what the projection reads back.
21
+ import { EMPTY_AGENT_SESSION_RESOURCES } from "./agent-sessions.js";
22
+ import { describeAgentSessionSchedule } from "./agent-session-schedule.js";
23
+ import { isCronSyntax } from "./schedule-label.js";
24
+ import { isValidTimeZone } from "./zoned-time.js";
25
+ export const TEAMMATE_PROPOSE_ROUTINE_TOOL_NAME = "propose_routine";
26
+ /** The runtime tool a routine's run reports into its creator's chat with. */
27
+ export const TEAMMATE_ROUTINE_POST_UPDATE_TOOL_NAME = "post_update";
28
+ /**
29
+ * How long an unattended teammate run (a routine's or a background task's)
30
+ * waits for its person to decide an approval. Then the approval expires, the
31
+ * run stops, and the chat says so; nothing waits on a person indefinitely.
32
+ */
33
+ export const TEAMMATE_UNATTENDED_APPROVAL_TTL_MINUTES = 60;
34
+ export const TEAMMATE_ROUTINE_LIMITS = {
35
+ nameChars: 80,
36
+ goalChars: 4_000,
37
+ whyChars: 300,
38
+ /** One report a run posts; a longer one belongs on a Knowledge page or a record. */
39
+ reportChars: 4_000,
40
+ /** The most often a proposed interval routine runs. A cron one runs at most hourly: one fixed minute. */
41
+ minIntervalMinutes: 15,
42
+ maxIntervalMinutes: 10_080,
43
+ maxPerRunCredits: 2_000,
44
+ maxDailyCredits: 20_000,
45
+ resourcesPerKind: 20,
46
+ };
47
+ const RESOURCE_KINDS = Object.keys(EMPTY_AGENT_SESSION_RESOURCES);
48
+ /** The tool's input schema, as the model reads it. */
49
+ export const TEAMMATE_PROPOSE_ROUTINE_INPUT_SCHEMA = {
50
+ type: "object",
51
+ additionalProperties: false,
52
+ properties: {
53
+ name: {
54
+ type: "string",
55
+ minLength: 1,
56
+ maxLength: TEAMMATE_ROUTINE_LIMITS.nameChars,
57
+ description: "A short name, e.g. \"Monday pipeline review\".",
58
+ },
59
+ goal: {
60
+ type: "string",
61
+ minLength: 1,
62
+ maxLength: TEAMMATE_ROUTINE_LIMITS.goalChars,
63
+ description: "What each run does without this chat: what to check, what is worth reporting, and what to leave alone.",
64
+ },
65
+ schedule: {
66
+ type: "object",
67
+ additionalProperties: false,
68
+ properties: {
69
+ kind: { type: "string", enum: ["interval", "cron"] },
70
+ every_minutes: {
71
+ type: "integer",
72
+ minimum: TEAMMATE_ROUTINE_LIMITS.minIntervalMinutes,
73
+ maximum: TEAMMATE_ROUTINE_LIMITS.maxIntervalMinutes,
74
+ description: "interval: minutes between runs.",
75
+ },
76
+ cron: { type: "string", description: "cron: five fields with one fixed minute, e.g. \"0 9 * * 1-5\" for weekdays at 09:00." },
77
+ timezone: { type: "string", description: "cron: the person's IANA time zone, e.g. Europe/Berlin." },
78
+ },
79
+ required: ["kind"],
80
+ },
81
+ per_run_credits: {
82
+ type: "integer",
83
+ minimum: 1,
84
+ maximum: TEAMMATE_ROUTINE_LIMITS.maxPerRunCredits,
85
+ description: "The most one run may spend, including inference.",
86
+ },
87
+ daily_credits: {
88
+ type: "integer",
89
+ minimum: 1,
90
+ maximum: TEAMMATE_ROUTINE_LIMITS.maxDailyCredits,
91
+ description: "The most its runs may spend in any 24 hours; at least per_run_credits.",
92
+ },
93
+ resources: {
94
+ type: "object",
95
+ additionalProperties: false,
96
+ properties: Object.fromEntries(RESOURCE_KINDS.map((kind) => [kind, {
97
+ type: "array", items: { type: "string" }, maxItems: TEAMMATE_ROUTINE_LIMITS.resourcesPerKind,
98
+ }])),
99
+ description: "Exact IDs or slugs each run may read: sequences (campaigns), tables, conversations, mailboxes, records, objects (CRM objects such as deals), knowledge (page slugs), domains.",
100
+ },
101
+ why: {
102
+ type: "string",
103
+ maxLength: TEAMMATE_ROUTINE_LIMITS.whyChars,
104
+ description: "One line on why this should run on a schedule.",
105
+ },
106
+ },
107
+ required: ["name", "goal", "schedule", "per_run_credits", "daily_credits"],
108
+ };
109
+ const clean = (value, max) => typeof value === "string" ? value.replace(/\s+/g, " ").trim().slice(0, max) : "";
110
+ const wholeNumber = (value) => (typeof value === "number" && Number.isInteger(value) ? value : NaN);
111
+ function normalizeSchedule(value) {
112
+ const schedule = value && typeof value === "object" && !Array.isArray(value) ? value : {};
113
+ if (schedule.kind === "cron") {
114
+ const cron = typeof schedule.cron === "string" ? schedule.cron.trim().replace(/\s+/g, " ") : "";
115
+ // One fixed minute keeps a cron routine to at most one run an hour; "every
116
+ // N minutes" is what an interval routine is for.
117
+ if (!isCronSyntax(cron) || !/^\d{1,2}$/.test(cron.split(" ")[0] ?? "") || Number(cron.split(" ")[0]) > 59) {
118
+ return { ok: false, message: "schedule.cron must be five cron fields with one fixed minute, e.g. \"0 9 * * 1-5\"." };
119
+ }
120
+ const timezone = typeof schedule.timezone === "string" ? schedule.timezone.trim() : "";
121
+ if (!isValidTimeZone(timezone))
122
+ return { ok: false, message: "schedule.timezone must be an IANA time zone, e.g. Europe/Berlin." };
123
+ return { ok: true, value: { kind: "cron", cron, timezone } };
124
+ }
125
+ if (schedule.kind === "interval") {
126
+ const every = wholeNumber(schedule.every_minutes);
127
+ if (!(every >= TEAMMATE_ROUTINE_LIMITS.minIntervalMinutes && every <= TEAMMATE_ROUTINE_LIMITS.maxIntervalMinutes)) {
128
+ return {
129
+ ok: false,
130
+ message: `schedule.every_minutes must be a whole number from ${TEAMMATE_ROUTINE_LIMITS.minIntervalMinutes} to ${TEAMMATE_ROUTINE_LIMITS.maxIntervalMinutes}.`,
131
+ };
132
+ }
133
+ return { ok: true, value: { kind: "interval", every_minutes: every } };
134
+ }
135
+ return { ok: false, message: "schedule.kind must be interval (every_minutes) or cron (cron and timezone)." };
136
+ }
137
+ function normalizeResources(value) {
138
+ if (value === undefined || value === null)
139
+ return { ok: true, value: {} };
140
+ if (typeof value !== "object" || Array.isArray(value))
141
+ return { ok: false, message: "resources must map resource kinds to ID or slug lists." };
142
+ const resources = {};
143
+ for (const [kind, refs] of Object.entries(value)) {
144
+ if (!RESOURCE_KINDS.includes(kind))
145
+ return { ok: false, message: `resources.${kind} is not a resource kind.` };
146
+ if (!Array.isArray(refs) || refs.length > TEAMMATE_ROUTINE_LIMITS.resourcesPerKind) {
147
+ return { ok: false, message: `resources.${kind} must list at most ${TEAMMATE_ROUTINE_LIMITS.resourcesPerKind} IDs or slugs.` };
148
+ }
149
+ const exact = refs.map((ref) => (typeof ref === "string" ? ref.trim() : ""));
150
+ if (exact.some((ref) => !ref || ref.includes(",") || ref === "*")) {
151
+ return { ok: false, message: `resources.${kind} takes exact IDs or slugs, never a wildcard.` };
152
+ }
153
+ if (exact.length > 0)
154
+ resources[kind] = [...new Set(exact)];
155
+ }
156
+ return { ok: true, value: resources };
157
+ }
158
+ /** Validate a proposal the model made, minting its id. */
159
+ export function normalizeTeammateRoutineProposal(args, mintId) {
160
+ const name = clean(args.name, TEAMMATE_ROUTINE_LIMITS.nameChars);
161
+ const goal = typeof args.goal === "string" ? args.goal.trim().slice(0, TEAMMATE_ROUTINE_LIMITS.goalChars) : "";
162
+ if (!name)
163
+ return { ok: false, message: "A routine needs a name." };
164
+ if (!goal)
165
+ return { ok: false, message: "A routine needs a goal each run can work from alone." };
166
+ const schedule = normalizeSchedule(args.schedule);
167
+ if (!schedule.ok)
168
+ return schedule;
169
+ const perRun = wholeNumber(args.per_run_credits);
170
+ const daily = wholeNumber(args.daily_credits);
171
+ if (!(perRun >= 1 && perRun <= TEAMMATE_ROUTINE_LIMITS.maxPerRunCredits)) {
172
+ return { ok: false, message: `per_run_credits must be a whole number from 1 to ${TEAMMATE_ROUTINE_LIMITS.maxPerRunCredits}.` };
173
+ }
174
+ if (!(daily >= perRun && daily <= TEAMMATE_ROUTINE_LIMITS.maxDailyCredits)) {
175
+ return { ok: false, message: `daily_credits must be a whole number from per_run_credits to ${TEAMMATE_ROUTINE_LIMITS.maxDailyCredits}.` };
176
+ }
177
+ const resources = normalizeResources(args.resources);
178
+ if (!resources.ok)
179
+ return resources;
180
+ const why = clean(args.why, TEAMMATE_ROUTINE_LIMITS.whyChars);
181
+ return {
182
+ ok: true,
183
+ proposal: {
184
+ id: mintId(), name, goal, schedule: schedule.value, per_run_credits: perRun, daily_credits: daily,
185
+ resources: resources.value, why: why || null,
186
+ },
187
+ };
188
+ }
189
+ /** Read a stored proposal back, refusing anything malformed. */
190
+ export function readTeammateRoutineProposal(value) {
191
+ if (!value || typeof value !== "object" || Array.isArray(value))
192
+ return null;
193
+ const record = value;
194
+ if (typeof record.id !== "string" || !record.id || record.id.length > 64)
195
+ return null;
196
+ const normalized = normalizeTeammateRoutineProposal(record, () => record.id);
197
+ return normalized.ok ? normalized.proposal : null;
198
+ }
199
+ /** The routine's schedule as a session reads it. */
200
+ export function teammateRoutineSessionSchedule(schedule) {
201
+ // A routine runs on the schedule its person approved, so its interval bounds
202
+ // are the interval itself: the run never moves its own next wake.
203
+ return schedule.kind === "cron"
204
+ ? { kind: "cron", cron: schedule.cron, timezone: schedule.timezone }
205
+ : { kind: "interval", interval_minutes: schedule.every_minutes, min_interval_minutes: schedule.every_minutes, max_interval_minutes: schedule.every_minutes };
206
+ }
207
+ /**
208
+ * The exact session settings the card creates for a proposal: a custom job
209
+ * with scoped reads only (writes ask the person each time), no events, and the
210
+ * marker that binds it to the person's chat.
211
+ */
212
+ export function teammateRoutineSessionConfig(proposal, chatId) {
213
+ return {
214
+ title: proposal.name,
215
+ goal: proposal.goal,
216
+ template: "custom",
217
+ resources: Object.fromEntries(RESOURCE_KINDS.map((kind) => [kind, [...(proposal.resources[kind] ?? [])]])),
218
+ actions: [],
219
+ events: [],
220
+ schedule: teammateRoutineSessionSchedule(proposal.schedule),
221
+ budget: { per_run_credits: proposal.per_run_credits, daily_credits: proposal.daily_credits },
222
+ teammate_routine: { chat_id: chatId, proposal_id: proposal.id },
223
+ };
224
+ }
225
+ /** The proposal as the turn's reply: what the CLI and MCP print, and what the next turn reads back. */
226
+ export function renderTeammateRoutineProposalText(proposal) {
227
+ const schedule = describeAgentSessionSchedule(teammateRoutineSessionSchedule(proposal.schedule));
228
+ return [
229
+ `I can set this up as a routine: **${proposal.name}**, ${schedule}, up to ${proposal.per_run_credits} credits a run and ${proposal.daily_credits} in any 24 hours.`,
230
+ ...(proposal.why ? [proposal.why] : []),
231
+ "It runs as you and posts here only when there is something new. Set it up from the card, or tell me what to change.",
232
+ ].join("\n\n");
233
+ }
234
+ /**
235
+ * The silence rule's comparison: a report that says what the routine's last
236
+ * report said, give or take whitespace, is not posted again.
237
+ */
238
+ export function sameTeammateRoutineReport(left, right) {
239
+ const flat = (text) => text.replace(/\s+/g, " ").trim();
240
+ return flat(left) === flat(right);
241
+ }
242
+ /**
243
+ * The routine proposal the chat is waiting on, or null. Like a question, only
244
+ * the newest turn can be waiting, and only when it completed by proposing; a
245
+ * proposal already set up (its id among `startedIds`) is not waiting.
246
+ */
247
+ export function projectPendingTeammateRoutineProposal(input) {
248
+ const newest = input.turns.reduce((latest, turn) => (latest === null || turn.turnIndex > latest.turnIndex ? turn : latest), null);
249
+ if (!newest || newest.status !== "completed")
250
+ return null;
251
+ for (let index = input.events.length - 1; index >= 0; index -= 1) {
252
+ const event = input.events[index];
253
+ if (!event || event.turnId !== newest.id || event.kind !== "tool_call_finished")
254
+ continue;
255
+ if (event.payload.tool !== TEAMMATE_PROPOSE_ROUTINE_TOOL_NAME)
256
+ continue;
257
+ const result = event.payload.result;
258
+ if (!result || typeof result !== "object" || result.ok === false)
259
+ continue;
260
+ const proposal = readTeammateRoutineProposal(result.proposal);
261
+ if (!proposal || input.startedIds.has(proposal.id))
262
+ return null;
263
+ return { ...proposal, turn_id: newest.id };
264
+ }
265
+ return null;
266
+ }
@@ -0,0 +1,56 @@
1
+ export declare const TEAMMATE_PROPOSE_TASK_TOOL_NAME = "propose_background_task";
2
+ export declare const TEAMMATE_TASK_LIMITS: {
3
+ readonly titleChars: 80;
4
+ readonly goalChars: 4000;
5
+ readonly whyChars: 300;
6
+ /** The highest ceiling a proposal may carry; a bigger job is several tasks. */
7
+ readonly maxCredits: 2000;
8
+ /** Background tasks one chat may have running at once. */
9
+ readonly activePerChat: 3;
10
+ };
11
+ export type TeammateTaskProposal = {
12
+ id: string;
13
+ title: string;
14
+ goal: string;
15
+ maxCredits: number;
16
+ why: string | null;
17
+ };
18
+ /** The proposal a chat is waiting on, as every surface reads it. */
19
+ export type PendingTeammateTaskProposal = TeammateTaskProposal & {
20
+ turnId: string;
21
+ };
22
+ /** The tool's input schema, as the model reads it. */
23
+ export declare const TEAMMATE_PROPOSE_TASK_INPUT_SCHEMA: Record<string, unknown>;
24
+ /** Validate a proposal the model made, minting its id. */
25
+ export declare function normalizeTeammateTaskProposal(args: Record<string, unknown>, mintId: () => string): {
26
+ ok: true;
27
+ proposal: TeammateTaskProposal;
28
+ } | {
29
+ ok: false;
30
+ message: string;
31
+ };
32
+ /** The proposal as the turn's reply: what the CLI and MCP print, and what the next turn reads back. */
33
+ export declare function renderTeammateTaskProposalText(proposal: TeammateTaskProposal): string;
34
+ /** Read a stored proposal back, refusing anything malformed. */
35
+ export declare function readTeammateTaskProposal(value: unknown): TeammateTaskProposal | null;
36
+ type ProjectionTurn = {
37
+ id: string;
38
+ status: string;
39
+ turnIndex: number;
40
+ };
41
+ type ProjectionEvent = {
42
+ turnId: string | null;
43
+ kind: string;
44
+ payload: Record<string, unknown>;
45
+ };
46
+ /**
47
+ * The proposal the chat is waiting on, or null. Like a question, only the
48
+ * newest turn can be waiting, and only when it completed by proposing; a
49
+ * proposal already started (its id among `startedIds`) is not waiting.
50
+ */
51
+ export declare function projectPendingTeammateTaskProposal(input: {
52
+ turns: readonly ProjectionTurn[];
53
+ events: readonly ProjectionEvent[];
54
+ startedIds: ReadonlySet<string>;
55
+ }): PendingTeammateTaskProposal | null;
56
+ export {};
@@ -0,0 +1,114 @@
1
+ // AI teammates (ADR 0032): background tasks.
2
+ //
3
+ // In a teammate chat the teammate can propose to run something in the
4
+ // background instead of making the person wait for it: an unattended run of
5
+ // the teammate's own published Agent, with a goal and a credit ceiling. The
6
+ // proposal ENDS the turn, the way a question card does; the person starts it
7
+ // from a card (or `oxygents task` on the CLI and MCP), which is the approval:
8
+ // the run acts as them, within the ceiling they accepted, and its result is
9
+ // posted back into the chat. Nothing starts on the model's word alone.
10
+ //
11
+ // One owner for the contract, because four readers depend on it: the tool that
12
+ // validates the model's proposal, the session read that projects the pending
13
+ // one, the card that starts it, and the route that turns it into a run.
14
+ export const TEAMMATE_PROPOSE_TASK_TOOL_NAME = "propose_background_task";
15
+ export const TEAMMATE_TASK_LIMITS = {
16
+ titleChars: 80,
17
+ goalChars: 4_000,
18
+ whyChars: 300,
19
+ /** The highest ceiling a proposal may carry; a bigger job is several tasks. */
20
+ maxCredits: 2_000,
21
+ /** Background tasks one chat may have running at once. */
22
+ activePerChat: 3,
23
+ };
24
+ /** The tool's input schema, as the model reads it. */
25
+ export const TEAMMATE_PROPOSE_TASK_INPUT_SCHEMA = {
26
+ type: "object",
27
+ additionalProperties: false,
28
+ properties: {
29
+ title: {
30
+ type: "string",
31
+ minLength: 1,
32
+ maxLength: TEAMMATE_TASK_LIMITS.titleChars,
33
+ description: "A short name for the task, e.g. \"Research 40 fintech accounts\".",
34
+ },
35
+ goal: {
36
+ type: "string",
37
+ minLength: 1,
38
+ maxLength: TEAMMATE_TASK_LIMITS.goalChars,
39
+ description: "Everything the run needs to do the work without this chat: the outcome, scope, inputs and where to put results.",
40
+ },
41
+ max_credits: {
42
+ type: "integer",
43
+ minimum: 1,
44
+ maximum: TEAMMATE_TASK_LIMITS.maxCredits,
45
+ description: "The most the run may spend, including inference. The person approves exactly this ceiling.",
46
+ },
47
+ why: {
48
+ type: "string",
49
+ maxLength: TEAMMATE_TASK_LIMITS.whyChars,
50
+ description: "One line on why this should run in the background rather than now.",
51
+ },
52
+ },
53
+ required: ["title", "goal", "max_credits"],
54
+ };
55
+ const clean = (value, max) => typeof value === "string" ? value.replace(/\s+/g, " ").trim().slice(0, max) : "";
56
+ /** Validate a proposal the model made, minting its id. */
57
+ export function normalizeTeammateTaskProposal(args, mintId) {
58
+ const title = clean(args.title, TEAMMATE_TASK_LIMITS.titleChars);
59
+ const goal = typeof args.goal === "string" ? args.goal.trim().slice(0, TEAMMATE_TASK_LIMITS.goalChars) : "";
60
+ const maxCredits = typeof args.max_credits === "number" && Number.isInteger(args.max_credits) ? args.max_credits : NaN;
61
+ if (!title)
62
+ return { ok: false, message: "A background task needs a title." };
63
+ if (!goal)
64
+ return { ok: false, message: "A background task needs a goal the run can work from alone." };
65
+ if (!(maxCredits >= 1 && maxCredits <= TEAMMATE_TASK_LIMITS.maxCredits)) {
66
+ return { ok: false, message: `max_credits must be a whole number from 1 to ${TEAMMATE_TASK_LIMITS.maxCredits}.` };
67
+ }
68
+ const why = clean(args.why, TEAMMATE_TASK_LIMITS.whyChars);
69
+ return { ok: true, proposal: { id: mintId(), title, goal, maxCredits, why: why || null } };
70
+ }
71
+ /** The proposal as the turn's reply: what the CLI and MCP print, and what the next turn reads back. */
72
+ export function renderTeammateTaskProposalText(proposal) {
73
+ return [
74
+ `I can run this in the background: **${proposal.title}**, up to ${proposal.maxCredits} credits.`,
75
+ ...(proposal.why ? [proposal.why] : []),
76
+ "It runs as you and its result posts here. Start it from the card, or tell me to do it differently.",
77
+ ].join("\n\n");
78
+ }
79
+ /** Read a stored proposal back, refusing anything malformed. */
80
+ export function readTeammateTaskProposal(value) {
81
+ if (!value || typeof value !== "object" || Array.isArray(value))
82
+ return null;
83
+ const record = value;
84
+ if (typeof record.id !== "string" || !record.id)
85
+ return null;
86
+ // The tool result keeps the normalized proposal, whose ceiling is `maxCredits`.
87
+ const normalized = normalizeTeammateTaskProposal({ ...record, max_credits: record.maxCredits ?? record.max_credits }, () => record.id);
88
+ return normalized.ok ? normalized.proposal : null;
89
+ }
90
+ /**
91
+ * The proposal the chat is waiting on, or null. Like a question, only the
92
+ * newest turn can be waiting, and only when it completed by proposing; a
93
+ * proposal already started (its id among `startedIds`) is not waiting.
94
+ */
95
+ export function projectPendingTeammateTaskProposal(input) {
96
+ const newest = input.turns.reduce((latest, turn) => (latest === null || turn.turnIndex > latest.turnIndex ? turn : latest), null);
97
+ if (!newest || newest.status !== "completed")
98
+ return null;
99
+ for (let index = input.events.length - 1; index >= 0; index -= 1) {
100
+ const event = input.events[index];
101
+ if (!event || event.turnId !== newest.id || event.kind !== "tool_call_finished")
102
+ continue;
103
+ if (event.payload.tool !== TEAMMATE_PROPOSE_TASK_TOOL_NAME)
104
+ continue;
105
+ const result = event.payload.result;
106
+ if (!result || typeof result !== "object" || result.ok === false)
107
+ continue;
108
+ const proposal = readTeammateTaskProposal(result.proposal);
109
+ if (!proposal || input.startedIds.has(proposal.id))
110
+ return null;
111
+ return { ...proposal, turnId: newest.id };
112
+ }
113
+ return null;
114
+ }
@@ -0,0 +1,78 @@
1
+ /**
2
+ * AI teammates (ADR 0032): Agents the whole workspace talks to, each person in
3
+ * one ongoing private chat per teammate, on the Copilot runtime.
4
+ *
5
+ * Staff-only on dev. `OXYGEN_TEAMMATES_ENABLED` is the switch; when it is
6
+ * unset the feature follows the Ongoing Agent sessions staff gate, which the
7
+ * dev web and worker already receive, because teammates re-scope those
8
+ * sessions into routines. Production and shadow never qualify.
9
+ */
10
+ export declare function teammatesEnabled(env?: Record<string, string | undefined>): boolean;
11
+ export declare function teammatesAvailableTo(env: Record<string, string | undefined>, viewer: {
12
+ email: string | null | undefined;
13
+ member: boolean;
14
+ oxygenRole?: string | null | undefined;
15
+ }): boolean;
16
+ /**
17
+ * The Workspace Agent's slug. Every workspace's first Oxygent is that Agent: an
18
+ * Oxygent like the others, first in the list and where Home opens, with no role
19
+ * of its own (Tim, 2026-10-13: "The default agent should be Oxygent and not
20
+ * Chief of Staff").
21
+ */
22
+ export declare const DEFAULT_TEAMMATE_SLUG = "workspace";
23
+ /** What an Oxygent is called until someone names it, as Grok Bot's are (Tim, 2026-10-13). */
24
+ export declare const NEW_TEAMMATE_NAME = "New Bot";
25
+ export declare const TEAMMATE_NAME_MAX_CHARS = 80;
26
+ export declare const TEAMMATE_DESCRIPTION_MAX_CHARS = 500;
27
+ /**
28
+ * An Oxygent's look, picked like a Grok Bot bot's (Tim, 2026-10-13): one of
29
+ * these shapes, each a little molecule of round atoms with eyes, in one of
30
+ * these colours. The web draws them; anything else stored reads as unset.
31
+ */
32
+ export declare const TEAMMATE_AVATAR_KEYS: readonly ["o2", "atom", "twin", "tripod", "mark", "clover", "trio", "bubbles", "crown", "orbit", "comet", "pill"];
33
+ export type TeammateAvatarKey = typeof TEAMMATE_AVATAR_KEYS[number];
34
+ export declare const TEAMMATE_AVATAR_COLORS: readonly ["forest", "brown", "red", "orange", "amber", "green", "aqua", "blue", "violet", "purple", "magenta", "slate"];
35
+ export type TeammateAvatarColor = typeof TEAMMATE_AVATAR_COLORS[number];
36
+ export declare function isTeammateAvatarKey(value: unknown): value is TeammateAvatarKey;
37
+ export declare function isTeammateAvatarColor(value: unknown): value is TeammateAvatarColor;
38
+ /**
39
+ * The look an Oxygent wears until someone picks one: the first Oxygent wears
40
+ * OXYGEN's own (an O2 molecule in forest green); any other one a look its slug
41
+ * picks, so a new one never arrives looking like the last.
42
+ */
43
+ export declare function teammateDefaultLook(slug: string): {
44
+ avatarKey: TeammateAvatarKey;
45
+ avatarColor: TeammateAvatarColor;
46
+ };
47
+ /**
48
+ * How an Oxygent presents: its name, what it does and its look. The first
49
+ * Oxygent is "New Bot" until its owner names it; nothing renames the stored
50
+ * Agent to get there, and the Workspace Agent's stock description is no job.
51
+ */
52
+ export declare function teammatePresentation(agent: {
53
+ slug: string;
54
+ name: string;
55
+ description?: string | null;
56
+ avatarKey?: string | null;
57
+ avatarColor?: string | null;
58
+ }): {
59
+ name: string;
60
+ description: string;
61
+ avatarKey: TeammateAvatarKey;
62
+ avatarColor: TeammateAvatarColor;
63
+ };
64
+ /** How much of a teammate's latest words its roster row shows. */
65
+ export declare const TEAMMATE_PREVIEW_MAX_CHARS = 140;
66
+ /**
67
+ * One line of a teammate's latest words, for its roster row: the markdown
68
+ * reduced to its text, whitespace collapsed, cut at a word near `maxChars`.
69
+ */
70
+ export declare function teammatePreviewLine(text: string, maxChars?: number): string;
71
+ /** A handle made from a teammate's name ("Lead Scout" → "lead-scout"); null when nothing usable is left. */
72
+ export declare function teammateSlugFromName(name: string): string | null;
73
+ /**
74
+ * The first instructions of an Oxygent added from the sidebar or the CLI. They
75
+ * name no name and no job: the chat's role block says both from the profile,
76
+ * which its owner edits at any time, where a published version would go stale.
77
+ */
78
+ export declare const TEAMMATE_STARTER_INSTRUCTIONS: string;
@@ -0,0 +1,140 @@
1
+ import { isStaffEmail } from "./staff.js";
2
+ /**
3
+ * AI teammates (ADR 0032): Agents the whole workspace talks to, each person in
4
+ * one ongoing private chat per teammate, on the Copilot runtime.
5
+ *
6
+ * Staff-only on dev. `OXYGEN_TEAMMATES_ENABLED` is the switch; when it is
7
+ * unset the feature follows the Ongoing Agent sessions staff gate, which the
8
+ * dev web and worker already receive, because teammates re-scope those
9
+ * sessions into routines. Production and shadow never qualify.
10
+ */
11
+ export function teammatesEnabled(env = process.env) {
12
+ if (env.OXYGEN_DEPLOY_ENV !== "selfhost-dev")
13
+ return false;
14
+ if (env.OXYGEN_AGENTS_ENABLED?.trim().toLowerCase() !== "true")
15
+ return false;
16
+ const flag = env.OXYGEN_TEAMMATES_ENABLED?.trim().toLowerCase();
17
+ if (flag)
18
+ return flag === "staff";
19
+ return env.OXYGEN_AGENT_SESSIONS_ENABLED === "staff";
20
+ }
21
+ export function teammatesAvailableTo(env, viewer) {
22
+ return teammatesEnabled(env) && viewer.member && viewer.oxygenRole !== "client" && isStaffEmail(viewer.email, env);
23
+ }
24
+ /**
25
+ * The Workspace Agent's slug. Every workspace's first Oxygent is that Agent: an
26
+ * Oxygent like the others, first in the list and where Home opens, with no role
27
+ * of its own (Tim, 2026-10-13: "The default agent should be Oxygent and not
28
+ * Chief of Staff").
29
+ */
30
+ export const DEFAULT_TEAMMATE_SLUG = "workspace";
31
+ /** What an Oxygent is called until someone names it, as Grok Bot's are (Tim, 2026-10-13). */
32
+ export const NEW_TEAMMATE_NAME = "New Bot";
33
+ const LEGACY_WORKSPACE_AGENT_NAME = "Workspace Agent";
34
+ const LEGACY_WORKSPACE_AGENT_DESCRIPTION = "General organization-grounded workspace worker.";
35
+ export const TEAMMATE_NAME_MAX_CHARS = 80;
36
+ export const TEAMMATE_DESCRIPTION_MAX_CHARS = 500;
37
+ /**
38
+ * An Oxygent's look, picked like a Grok Bot bot's (Tim, 2026-10-13): one of
39
+ * these shapes, each a little molecule of round atoms with eyes, in one of
40
+ * these colours. The web draws them; anything else stored reads as unset.
41
+ */
42
+ export const TEAMMATE_AVATAR_KEYS = [
43
+ "o2", "atom", "twin", "tripod", "mark", "clover", "trio", "bubbles", "crown", "orbit", "comet", "pill",
44
+ ];
45
+ export const TEAMMATE_AVATAR_COLORS = [
46
+ "forest", "brown", "red", "orange", "amber", "green", "aqua", "blue", "violet", "purple", "magenta", "slate",
47
+ ];
48
+ export function isTeammateAvatarKey(value) {
49
+ return typeof value === "string" && TEAMMATE_AVATAR_KEYS.includes(value);
50
+ }
51
+ export function isTeammateAvatarColor(value) {
52
+ return typeof value === "string" && TEAMMATE_AVATAR_COLORS.includes(value);
53
+ }
54
+ /**
55
+ * The look an Oxygent wears until someone picks one: the first Oxygent wears
56
+ * OXYGEN's own (an O2 molecule in forest green); any other one a look its slug
57
+ * picks, so a new one never arrives looking like the last.
58
+ */
59
+ export function teammateDefaultLook(slug) {
60
+ if (slug === DEFAULT_TEAMMATE_SLUG)
61
+ return { avatarKey: "o2", avatarColor: "forest" };
62
+ let hash = 2166136261;
63
+ for (const char of slug) {
64
+ hash ^= char.charCodeAt(0);
65
+ hash = Math.imul(hash, 16777619);
66
+ }
67
+ const unsigned = hash >>> 0;
68
+ return {
69
+ avatarKey: TEAMMATE_AVATAR_KEYS[unsigned % TEAMMATE_AVATAR_KEYS.length],
70
+ avatarColor: TEAMMATE_AVATAR_COLORS[(unsigned >>> 8) % TEAMMATE_AVATAR_COLORS.length],
71
+ };
72
+ }
73
+ /**
74
+ * How an Oxygent presents: its name, what it does and its look. The first
75
+ * Oxygent is "New Bot" until its owner names it; nothing renames the stored
76
+ * Agent to get there, and the Workspace Agent's stock description is no job.
77
+ */
78
+ export function teammatePresentation(agent) {
79
+ const first = agent.slug === DEFAULT_TEAMMATE_SLUG;
80
+ const name = first && agent.name === LEGACY_WORKSPACE_AGENT_NAME ? NEW_TEAMMATE_NAME : agent.name;
81
+ const stored = agent.description?.trim() ?? "";
82
+ const description = first && stored === LEGACY_WORKSPACE_AGENT_DESCRIPTION ? "" : stored;
83
+ const look = teammateDefaultLook(agent.slug);
84
+ return {
85
+ name,
86
+ description,
87
+ avatarKey: isTeammateAvatarKey(agent.avatarKey) ? agent.avatarKey : look.avatarKey,
88
+ avatarColor: isTeammateAvatarColor(agent.avatarColor) ? agent.avatarColor : look.avatarColor,
89
+ };
90
+ }
91
+ /** How much of a teammate's latest words its roster row shows. */
92
+ export const TEAMMATE_PREVIEW_MAX_CHARS = 140;
93
+ /**
94
+ * One line of a teammate's latest words, for its roster row: the markdown
95
+ * reduced to its text, whitespace collapsed, cut at a word near `maxChars`.
96
+ */
97
+ export function teammatePreviewLine(text, maxChars = TEAMMATE_PREVIEW_MAX_CHARS) {
98
+ const plain = text
99
+ .replace(/```[\s\S]*?(?:```|$)/g, " ")
100
+ .replace(/<!--[\s\S]*?-->/g, " ")
101
+ .replace(/!\[([^\]]*)\]\([^)]*\)/g, "$1")
102
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, "$1")
103
+ .replace(/`([^`]*)`/g, "$1")
104
+ // A table's rule row or a horizontal rule carries no words.
105
+ .replace(/^[ \t]*[-:|* \t]{3,}$/gm, " ")
106
+ .replace(/^[ \t]{0,3}(?:#{1,6}|>|[-*+]|\d+[.)])[ \t]+/gm, "")
107
+ .replace(/(\*\*|__|~~)(?=\S)([\s\S]*?\S)\1/g, "$2")
108
+ .replace(/(^|[^\w*])([*_])(?=\S)([^*_\n]*?\S)\2(?![\w*])/g, "$1$3")
109
+ .replace(/\|/g, " ")
110
+ .replace(/\s+/g, " ")
111
+ .trim();
112
+ if (plain.length <= maxChars)
113
+ return plain;
114
+ const cut = plain.slice(0, maxChars);
115
+ // A cut that lands on a space already ends on a whole word.
116
+ const space = plain[maxChars] === " " ? maxChars : cut.lastIndexOf(" ");
117
+ const word = space > maxChars * 0.6 ? cut.slice(0, space) : cut;
118
+ return `${word.replace(/[\s,;:.\-–—]+$/, "")}…`;
119
+ }
120
+ /** A handle made from a teammate's name ("Lead Scout" → "lead-scout"); null when nothing usable is left. */
121
+ export function teammateSlugFromName(name) {
122
+ const slug = name
123
+ .normalize("NFKD")
124
+ .replace(/[̀-ͯ]/g, "")
125
+ .toLowerCase()
126
+ .replace(/[^a-z0-9]+/g, "-")
127
+ .replace(/^[^a-z]+/, "")
128
+ .slice(0, 40)
129
+ .replace(/-+$/, "");
130
+ return /^[a-z][a-z0-9-]{1,48}$/.test(slug) ? slug : null;
131
+ }
132
+ /**
133
+ * The first instructions of an Oxygent added from the sidebar or the CLI. They
134
+ * name no name and no job: the chat's role block says both from the profile,
135
+ * which its owner edits at any time, where a published version would go stale.
136
+ */
137
+ export const TEAMMATE_STARTER_INSTRUCTIONS = [
138
+ "You are an Oxygent on the team. Do the job your profile and the person give you, and hand work that is another Oxygent's job to them.",
139
+ "Read whatever you need from the workspace; ask before anything that writes, sends or spends.",
140
+ ].join("\n");