@oxygen-agent/cli 1.936.1 → 1.982.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +1 -1
  2. package/dist/admin-primary-providers-render.js +9 -1
  3. package/dist/cli-values.d.ts +14 -0
  4. package/dist/cli-values.js +26 -0
  5. package/dist/command-manifest.js +30 -2
  6. package/dist/functions-commands.js +13 -5
  7. package/dist/help.js +2 -0
  8. package/dist/index.js +1509 -290
  9. package/dist/knowledge-repository-commands.d.ts +6 -0
  10. package/dist/knowledge-repository-commands.js +198 -0
  11. package/dist/skills.js +20 -0
  12. package/dist/ugc-commands.js +470 -15
  13. package/node_modules/@oxygen/recipe-sdk/dist/index.d.ts +2 -0
  14. package/node_modules/@oxygen/shared/dist/byok-connect.d.ts +11 -6
  15. package/node_modules/@oxygen/shared/dist/byok-connect.js +14 -6
  16. package/node_modules/@oxygen/shared/dist/capability-discovery.d.ts +8 -0
  17. package/node_modules/@oxygen/shared/dist/capability-discovery.js +152 -20
  18. package/node_modules/@oxygen/shared/dist/copilot-errors.js +3 -0
  19. package/node_modules/@oxygen/shared/dist/copilot-journeys.d.ts +19 -1
  20. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.d.ts +19 -0
  21. package/node_modules/@oxygen/shared/dist/copilot-journeys.generated.js +26 -0
  22. package/node_modules/@oxygen/shared/dist/copilot-journeys.js +8 -41
  23. package/node_modules/@oxygen/shared/dist/email-dsn.d.ts +60 -0
  24. package/node_modules/@oxygen/shared/dist/email-dsn.js +120 -0
  25. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.d.ts +64 -0
  26. package/node_modules/@oxygen/shared/dist/email-warmup-readiness.js +90 -0
  27. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.d.ts +28 -0
  28. package/node_modules/@oxygen/shared/dist/inbox-avatar-url.js +57 -0
  29. package/node_modules/@oxygen/shared/dist/index.d.ts +10 -0
  30. package/node_modules/@oxygen/shared/dist/index.js +10 -0
  31. package/node_modules/@oxygen/shared/dist/knowledge-bases.d.ts +74 -0
  32. package/node_modules/@oxygen/shared/dist/knowledge-bases.js +456 -0
  33. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +56 -48
  34. package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +50 -49
  35. package/node_modules/@oxygen/shared/dist/knowledge-repository.d.ts +22 -0
  36. package/node_modules/@oxygen/shared/dist/knowledge-repository.js +121 -0
  37. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.d.ts +20 -0
  38. package/node_modules/@oxygen/shared/dist/knowledge-vault-markdown.js +155 -0
  39. package/node_modules/@oxygen/shared/dist/langfuse.d.ts +8 -3
  40. package/node_modules/@oxygen/shared/dist/langfuse.js +177 -130
  41. package/node_modules/@oxygen/shared/dist/llm-payload.d.ts +10 -0
  42. package/node_modules/@oxygen/shared/dist/llm-payload.js +54 -0
  43. package/node_modules/@oxygen/shared/dist/llm-usage.d.ts +11 -0
  44. package/node_modules/@oxygen/shared/dist/llm-usage.js +30 -0
  45. package/node_modules/@oxygen/shared/dist/mailbox-import.d.ts +10 -0
  46. package/node_modules/@oxygen/shared/dist/mailbox-import.js +53 -0
  47. package/node_modules/@oxygen/shared/dist/plan-limits.d.ts +8 -0
  48. package/node_modules/@oxygen/shared/dist/plan-limits.js +8 -0
  49. package/node_modules/@oxygen/shared/dist/pricing-sheet.d.ts +1 -1
  50. package/node_modules/@oxygen/shared/dist/pricing-sheet.js +1 -1
  51. package/node_modules/@oxygen/shared/dist/product-analytics-core.d.ts +98 -0
  52. package/node_modules/@oxygen/shared/dist/product-analytics-core.js +159 -0
  53. package/node_modules/@oxygen/shared/dist/product-analytics-environment.d.ts +18 -0
  54. package/node_modules/@oxygen/shared/dist/product-analytics-environment.js +46 -0
  55. package/node_modules/@oxygen/shared/dist/product-analytics-events.d.ts +116 -0
  56. package/node_modules/@oxygen/shared/dist/product-analytics-events.js +120 -0
  57. package/node_modules/@oxygen/shared/dist/recipes.d.ts +6 -0
  58. package/node_modules/@oxygen/shared/dist/recipes.js +23 -0
  59. package/node_modules/@oxygen/shared/dist/sequences.d.ts +126 -2
  60. package/node_modules/@oxygen/shared/dist/sequences.js +280 -4
  61. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.d.ts +2 -0
  62. package/node_modules/@oxygen/shared/dist/ugc-amplification-identity.js +24 -0
  63. package/node_modules/@oxygen/shared/dist/ugc.d.ts +29 -1
  64. package/node_modules/@oxygen/shared/dist/user-capability-routing.js +8 -1
  65. package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
  66. package/node_modules/@oxygen/shared/dist/version.js +3 -1
  67. package/node_modules/@oxygen/shared/dist/workspace-file-storage.d.ts +6 -2
  68. package/node_modules/@oxygen/shared/dist/workspace-file-storage.js +15 -4
  69. package/node_modules/@oxygen/shared/package.json +15 -0
  70. package/node_modules/@oxygen/workflows/dist/graph/lint.js +22 -0
  71. package/package.json +2 -1
@@ -0,0 +1,456 @@
1
+ import { parseDocument } from "yaml";
2
+ function record(value) {
3
+ return value !== null && typeof value === "object" && !Array.isArray(value)
4
+ ? value
5
+ : null;
6
+ }
7
+ function stringValue(value) {
8
+ return typeof value === "string" && value.trim() ? value.trim() : null;
9
+ }
10
+ function strings(value) {
11
+ return Array.isArray(value)
12
+ ? value
13
+ .filter((item) => typeof item === "string" && item.trim().length > 0)
14
+ .map((item) => item.trim())
15
+ : [];
16
+ }
17
+ function direction(value) {
18
+ return typeof value === "string" && value.toUpperCase() === "DESC"
19
+ ? "DESC"
20
+ : "ASC";
21
+ }
22
+ function filterNode(value, issues, location) {
23
+ if (typeof value === "string")
24
+ return value;
25
+ const source = record(value);
26
+ if (!source) {
27
+ if (value !== undefined)
28
+ issues.push({
29
+ code: "unsupported_filter",
30
+ message: `${location} is preserved but has an unsupported filter shape.`,
31
+ });
32
+ return undefined;
33
+ }
34
+ const result = {};
35
+ const knownKeys = new Set(["and", "or", "not"]);
36
+ for (const key of Object.keys(source)) {
37
+ if (!knownKeys.has(key))
38
+ issues.push({
39
+ code: "unsupported_filter",
40
+ message: `${location}.${key} is preserved but is not supported.`,
41
+ });
42
+ }
43
+ for (const key of ["and", "or", "not"]) {
44
+ if (source[key] === undefined)
45
+ continue;
46
+ if (!Array.isArray(source[key])) {
47
+ issues.push({
48
+ code: "unsupported_filter",
49
+ message: `${location}.${key} must be a list.`,
50
+ });
51
+ continue;
52
+ }
53
+ const children = source[key].flatMap((child) => {
54
+ const parsed = filterNode(child, issues, `${location}.${key}`);
55
+ return parsed === undefined ? [] : [parsed];
56
+ });
57
+ result[key] = children;
58
+ }
59
+ return Object.keys(result).length > 0 ? result : undefined;
60
+ }
61
+ function mapRecord(value, map) {
62
+ const source = record(value);
63
+ if (!source)
64
+ return {};
65
+ return Object.fromEntries(Object.entries(source).flatMap(([key, item]) => {
66
+ const mapped = map(key, item);
67
+ return mapped === null ? [] : [[key, mapped]];
68
+ }));
69
+ }
70
+ function parseView(value, index, issues) {
71
+ const source = record(value);
72
+ if (!source) {
73
+ issues.push({
74
+ code: "invalid_view",
75
+ message: `View ${index + 1} is not an object.`,
76
+ });
77
+ return null;
78
+ }
79
+ const type = stringValue(source.type) ?? "table";
80
+ const name = stringValue(source.name) ?? `View ${index + 1}`;
81
+ if (!["table", "cards", "kanban"].includes(type)) {
82
+ issues.push({
83
+ code: "unsupported_view",
84
+ message: `The ${type} view type is preserved but cannot be rendered.`,
85
+ view: name,
86
+ });
87
+ }
88
+ const group = record(source.groupBy);
89
+ const groupProperty = stringValue(group?.property);
90
+ const view = {
91
+ type,
92
+ name,
93
+ order: strings(source.order),
94
+ boardColumns: strings(source.boardColumns),
95
+ sort: Array.isArray(source.sort)
96
+ ? source.sort.flatMap((item) => {
97
+ const sort = record(item);
98
+ const property = stringValue(sort?.property);
99
+ return property
100
+ ? [{ property, direction: direction(sort?.direction) }]
101
+ : [];
102
+ })
103
+ : [],
104
+ columnColors: mapRecord(source.columnColors, (_key, item) => typeof item === "string" ? item : null),
105
+ wipLimits: mapRecord(source.wipLimits, (_key, item) => typeof item === "number" && Number.isFinite(item) ? item : null),
106
+ raw: source,
107
+ };
108
+ if (groupProperty)
109
+ view.groupBy = {
110
+ property: groupProperty,
111
+ direction: direction(group?.direction),
112
+ };
113
+ const cardTitleProperty = stringValue(source.cardTitleProperty);
114
+ if (cardTitleProperty)
115
+ view.cardTitleProperty = cardTitleProperty;
116
+ const filters = filterNode(source.filters, issues, `${name}.filters`);
117
+ if (filters)
118
+ view.filters = filters;
119
+ return view;
120
+ }
121
+ export function parseKnowledgeBase(path, source) {
122
+ const document = parseDocument(source, { customTags: [] });
123
+ if (document.errors.length > 0) {
124
+ return {
125
+ definition: null,
126
+ issues: document.errors.map((error) => ({
127
+ code: "invalid_base",
128
+ message: error.message,
129
+ })),
130
+ };
131
+ }
132
+ let parsed;
133
+ try {
134
+ parsed = document.toJS({ maxAliasCount: 0 });
135
+ }
136
+ catch (error) {
137
+ return {
138
+ definition: null,
139
+ issues: [
140
+ {
141
+ code: "invalid_base",
142
+ message: error instanceof Error
143
+ ? error.message
144
+ : "The base document could not be decoded.",
145
+ },
146
+ ],
147
+ };
148
+ }
149
+ const root = record(parsed);
150
+ if (!root)
151
+ return {
152
+ definition: null,
153
+ issues: [
154
+ {
155
+ code: "invalid_base",
156
+ message: "The base document must be a YAML object.",
157
+ },
158
+ ],
159
+ };
160
+ const issues = [];
161
+ const views = Array.isArray(root.views)
162
+ ? root.views.flatMap((value, index) => {
163
+ const view = parseView(value, index, issues);
164
+ return view ? [view] : [];
165
+ })
166
+ : [];
167
+ const properties = mapRecord(root.properties, (_key, item) => {
168
+ const raw = record(item) ?? {};
169
+ const displayName = stringValue(raw.displayName);
170
+ return { ...(displayName ? { displayName } : {}), raw };
171
+ });
172
+ const name = path
173
+ .split("/")
174
+ .pop()
175
+ ?.replace(/\.base$/i, "") || path;
176
+ const definition = {
177
+ path,
178
+ name,
179
+ properties,
180
+ views,
181
+ raw: root,
182
+ };
183
+ const filters = filterNode(root.filters, issues, `${name}.filters`);
184
+ if (filters)
185
+ definition.filters = filters;
186
+ return { definition, issues };
187
+ }
188
+ export function knowledgeBasePropertyKey(property) {
189
+ return property.startsWith("note.") ? property.slice(5) : property;
190
+ }
191
+ function fileName(path) {
192
+ const name = path.split("/").pop() ?? path;
193
+ return name.replace(/\.[^.]+$/, "");
194
+ }
195
+ export function knowledgeBasePropertyValue(file, property) {
196
+ if (property === "file.name")
197
+ return fileName(file.path);
198
+ if (property === "file.path")
199
+ return file.path;
200
+ if (property === "file.ext")
201
+ return file.path.includes(".")
202
+ ? (file.path.split(".").pop()?.toLowerCase() ?? "")
203
+ : "";
204
+ return file.properties[knowledgeBasePropertyKey(property)];
205
+ }
206
+ export function knowledgeBasePropertyText(file, property) {
207
+ const value = knowledgeBasePropertyValue(file, property);
208
+ if (value == null)
209
+ return "";
210
+ if (Array.isArray(value))
211
+ return value.map(String).join(", ");
212
+ if (typeof value === "object")
213
+ return JSON.stringify(value);
214
+ return String(value);
215
+ }
216
+ function isEmpty(value) {
217
+ return (value == null ||
218
+ value === "" ||
219
+ (Array.isArray(value) && value.length === 0));
220
+ }
221
+ function equals(left, right) {
222
+ if (Array.isArray(left))
223
+ return left.some((item) => String(item) === right);
224
+ return String(left ?? "") === right;
225
+ }
226
+ function compare(left, right) {
227
+ if (typeof left === "number" && typeof right === "number")
228
+ return left - right;
229
+ const leftNumber = typeof left === "string" && left.trim() ? Number(left) : Number.NaN;
230
+ const rightNumber = typeof right === "string" && right.trim() ? Number(right) : Number.NaN;
231
+ if (Number.isFinite(leftNumber) && Number.isFinite(rightNumber))
232
+ return leftNumber - rightNumber;
233
+ return String(left ?? "").localeCompare(String(right ?? ""), undefined, {
234
+ numeric: true,
235
+ sensitivity: "base",
236
+ });
237
+ }
238
+ function evaluatePredicate(expression, file) {
239
+ let match;
240
+ if ((match = /^file\.inFolder\(\s*"([^"]*)"\s*\)$/.exec(expression))) {
241
+ const folder = (match[1] ?? "").replace(/^\/+|\/+$/g, "");
242
+ return { matches: file.path.startsWith(`${folder}/`), supported: true };
243
+ }
244
+ if ((match = /^([\w.]+)\.(isEmpty|isNotEmpty)\(\)$/.exec(expression))) {
245
+ const empty = isEmpty(knowledgeBasePropertyValue(file, match[1] ?? ""));
246
+ return {
247
+ matches: match[2] === "isEmpty" ? empty : !empty,
248
+ supported: true,
249
+ };
250
+ }
251
+ if ((match = /^([\w.]+)\.contains\(\s*"([^"]*)"\s*\)$/.exec(expression))) {
252
+ const value = knowledgeBasePropertyValue(file, match[1] ?? "");
253
+ const needle = match[2] ?? "";
254
+ return {
255
+ matches: Array.isArray(value)
256
+ ? value.some((item) => String(item).includes(needle))
257
+ : String(value ?? "").includes(needle),
258
+ supported: true,
259
+ };
260
+ }
261
+ if ((match =
262
+ /^([\w.]+)\s*(==|!=|>=|<=|>|<)\s*(?:"([^"]*)"|(-?\d+(?:\.\d+)?))$/.exec(expression))) {
263
+ const value = knowledgeBasePropertyValue(file, match[1] ?? "");
264
+ const operand = match[3] ?? match[4] ?? "";
265
+ const operator = match[2];
266
+ if (operator === "==" || operator === "!=") {
267
+ const equal = equals(value, operand);
268
+ return { matches: operator === "==" ? equal : !equal, supported: true };
269
+ }
270
+ const numeric = Number(value);
271
+ const expected = Number(operand);
272
+ const matches = Number.isFinite(numeric) &&
273
+ Number.isFinite(expected) &&
274
+ (operator === ">="
275
+ ? numeric >= expected
276
+ : operator === "<="
277
+ ? numeric <= expected
278
+ : operator === ">"
279
+ ? numeric > expected
280
+ : numeric < expected);
281
+ return { matches, supported: true };
282
+ }
283
+ return { matches: true, supported: false };
284
+ }
285
+ function evaluateFilter(node, file, unsupported) {
286
+ if (node === undefined)
287
+ return true;
288
+ if (typeof node === "string") {
289
+ const result = evaluatePredicate(node.trim(), file);
290
+ if (!result.supported)
291
+ unsupported.add(node);
292
+ return result.matches;
293
+ }
294
+ if (node.and)
295
+ return node.and
296
+ .map((child) => evaluateFilter(child, file, unsupported))
297
+ .every(Boolean);
298
+ if (node.or)
299
+ return node.or
300
+ .map((child) => evaluateFilter(child, file, unsupported))
301
+ .some(Boolean);
302
+ if (node.not)
303
+ return !node.not
304
+ .map((child) => evaluateFilter(child, file, unsupported))
305
+ .every(Boolean);
306
+ return true;
307
+ }
308
+ function collectUnsupportedFilters(node, unsupported) {
309
+ if (node === undefined)
310
+ return;
311
+ if (typeof node === "string") {
312
+ if (!evaluatePredicate(node.trim(), {
313
+ id: "",
314
+ path: "",
315
+ properties: {},
316
+ revision: 0,
317
+ }).supported)
318
+ unsupported.add(node);
319
+ return;
320
+ }
321
+ for (const child of [
322
+ ...(node.and ?? []),
323
+ ...(node.or ?? []),
324
+ ...(node.not ?? []),
325
+ ]) {
326
+ collectUnsupportedFilters(child, unsupported);
327
+ }
328
+ }
329
+ function stableSort(files, sort, includeKanbanOrder) {
330
+ const rules = includeKanbanOrder
331
+ ? [{ property: "kanban_order", direction: "ASC" }, ...sort]
332
+ : sort;
333
+ return files
334
+ .map((file, index) => ({ file, index }))
335
+ .sort((left, right) => {
336
+ for (const rule of rules) {
337
+ const compared = compare(knowledgeBasePropertyValue(left.file, rule.property), knowledgeBasePropertyValue(right.file, rule.property));
338
+ if (compared !== 0)
339
+ return rule.direction === "DESC" ? -compared : compared;
340
+ }
341
+ return left.index - right.index;
342
+ })
343
+ .map(({ file }) => file);
344
+ }
345
+ export function evaluateKnowledgeBaseView(definition, view, files) {
346
+ const unsupported = new Set();
347
+ collectUnsupportedFilters(definition.filters, unsupported);
348
+ collectUnsupportedFilters(view.filters, unsupported);
349
+ const matched = files.filter((file) => evaluateFilter(definition.filters, file, unsupported) &&
350
+ evaluateFilter(view.filters, file, unsupported));
351
+ return {
352
+ files: stableSort(matched, view.sort, view.type === "kanban"),
353
+ issues: [...unsupported].map((expression) => ({
354
+ code: "unsupported_filter",
355
+ message: `This filter is preserved but is not supported: ${expression}`,
356
+ expression,
357
+ view: view.name,
358
+ })),
359
+ };
360
+ }
361
+ export function knowledgeBaseGroups(view, files, configuredColumns = []) {
362
+ const property = view.groupBy?.property;
363
+ if (!property)
364
+ return [{ key: "__all", title: view.name, files: [...files] }];
365
+ const groupDirection = view.groupBy?.direction ?? "ASC";
366
+ const discovered = [
367
+ ...new Set(files
368
+ .map((file) => knowledgeBasePropertyText(file, property).trim())
369
+ .filter(Boolean)),
370
+ ];
371
+ const ordered = configuredColumns.length > 0
372
+ ? [...configuredColumns]
373
+ : view.boardColumns.length > 0
374
+ ? [...view.boardColumns]
375
+ : discovered.sort((left, right) => left.localeCompare(right));
376
+ const configured = new Set(ordered.map((value) => value.toLowerCase()));
377
+ ordered.push(...discovered
378
+ .filter((value) => !configured.has(value.toLowerCase()))
379
+ .sort((left, right) => left.localeCompare(right)));
380
+ if (groupDirection === "DESC")
381
+ ordered.reverse();
382
+ const noValueLabels = new Set(["(no value)", "(none)", "(empty)"]);
383
+ const groups = ordered.map((value) => {
384
+ const isNoValue = noValueLabels.has(value.toLowerCase());
385
+ return {
386
+ key: isNoValue ? "__empty" : value,
387
+ title: value,
388
+ files: files.filter((file) => {
389
+ const current = knowledgeBasePropertyText(file, property).trim();
390
+ return isNoValue
391
+ ? !current
392
+ : current.toLowerCase() === value.toLowerCase();
393
+ }),
394
+ };
395
+ });
396
+ const known = new Set(ordered
397
+ .filter((value) => !noValueLabels.has(value.toLowerCase()))
398
+ .map((value) => value.toLowerCase()));
399
+ const hasEmptyColumn = ordered.some((value) => noValueLabels.has(value.toLowerCase()));
400
+ const unmatched = files.filter((file) => {
401
+ const current = knowledgeBasePropertyText(file, property).trim();
402
+ return current ? !known.has(current.toLowerCase()) : !hasEmptyColumn;
403
+ });
404
+ if (unmatched.length > 0)
405
+ groups.push({
406
+ key: "__empty",
407
+ title: `(No ${knowledgeBasePropertyKey(property)})`,
408
+ files: unmatched,
409
+ });
410
+ return groups;
411
+ }
412
+ export function knowledgeBaseDisplayName(definition, property) {
413
+ const key = knowledgeBasePropertyKey(property);
414
+ return (definition.properties[property]?.displayName ??
415
+ definition.properties[`note.${key}`]?.displayName ??
416
+ definition.properties[key]?.displayName ??
417
+ (property === "file.name" ? "Title" : key));
418
+ }
419
+ export function knowledgeBaseCardTitle(file, view) {
420
+ return (knowledgeBasePropertyText(file, view.cardTitleProperty ?? "file.name") ||
421
+ fileName(file.path));
422
+ }
423
+ export function parseBaseBoardColumnOrder(value) {
424
+ const root = record(value);
425
+ const configs = record(root?.columnConfigs);
426
+ if (!configs)
427
+ return {};
428
+ return mapRecord(configs, (_key, item) => {
429
+ const columns = strings(record(item)?.columns);
430
+ return columns.length > 0 ? columns : null;
431
+ });
432
+ }
433
+ function inFolderExpressions(node) {
434
+ if (node === undefined)
435
+ return [];
436
+ if (typeof node === "string") {
437
+ const match = /^file\.inFolder\(\s*"([^"]*)"\s*\)$/.exec(node.trim());
438
+ return match?.[1] ? [match[1].replace(/^\/+|\/+$/g, "")] : [];
439
+ }
440
+ return [...(node.and ?? []), ...(node.or ?? []), ...(node.not ?? [])].flatMap(inFolderExpressions);
441
+ }
442
+ /** Resolve Base Board's `<folder>::<view>::<property>` lane order, with view-name shorthand for API callers. */
443
+ export function resolveBaseBoardColumnOrder(definition, view, configured) {
444
+ const property = knowledgeBasePropertyKey(view.groupBy?.property ?? "status");
445
+ const direct = configured[view.name];
446
+ if (direct)
447
+ return [...direct];
448
+ for (const folder of inFolderExpressions(definition.filters)) {
449
+ const exact = configured[`${folder}::${view.name}::${property}`];
450
+ if (exact)
451
+ return [...exact];
452
+ }
453
+ const suffix = `::${view.name}::${property}`;
454
+ const candidates = Object.entries(configured).filter(([key]) => key.endsWith(suffix));
455
+ return candidates.length === 1 ? [...candidates[0][1]] : [];
456
+ }
@@ -2,28 +2,35 @@
2
2
  * Knowledge bootstrap — the once-per-workspace, automatic company research pass.
3
3
  *
4
4
  * WHAT IT IS. Exactly once in a workspace's life, OXYGEN researches the customer's
5
- * OWN company from the domain we already resolved at org creation
6
- * (control-DB `organizations.iconDomain`, `iconStatus = 'ok'`), then fills the typed
7
- * company profile (`ox_context.company_profile`) and writes one cited wiki page.
8
- * A founder who signs up should not face an empty Knowledge Graph and have to type
9
- * their own positioning back at us; every grounded AI action downstream (message
10
- * drafts, AI columns, agent runs) reads that profile, so an empty one degrades the
11
- * whole product's first hour.
5
+ * OWN company from the domain the creator typed when the workspace was created
6
+ * (control-DB `organizations.iconDomain`), then fills the typed company profile
7
+ * (`ox_context.company_profile`) and writes one cited wiki page. A founder who
8
+ * signs up should not face an empty Knowledge Graph and have to type their own
9
+ * positioning back at us; every grounded AI action downstream (message drafts, AI
10
+ * columns, agent runs) reads that profile, so an empty one degrades the whole
11
+ * product's first hour.
12
12
  *
13
13
  * WHY THIS IS NOT A VIOLATION OF THE PAID-ACTIONS RULE. `CLAUDE.md` says never run
14
14
  * paid provider actions unless the user explicitly asks. This slice is a deliberate,
15
- * founder-approved exception: signup IS the standing authorization, the same way an
16
- * armed table-webhook auto-run configuration is scoped standing permission for the
17
- * columns it queues. The exception is defensible ONLY because every one of the
18
- * following properties holds. They are load-bearing — do not drop one for
15
+ * founder-approved exception: naming the company at workspace creation IS the
16
+ * standing authorization, the same way an armed table-webhook auto-run
17
+ * configuration is scoped standing permission for the columns it queues — and
18
+ * since 2026-09-11 the pass is OXYGEN-funded, so it never draws down the
19
+ * customer's balance at all. The exception is defensible ONLY because every one of
20
+ * the following properties holds. They are load-bearing — do not drop one for
19
21
  * convenience, and if you remove one, the exception no longer stands:
20
22
  *
21
23
  * 1. CAPPED — `KNOWLEDGE_BOOTSTRAP_MAX_CREDITS` is a hard ceiling on the whole
22
24
  * pass, with `KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS` a tighter
23
- * sub-ceiling on the provider (external-money) half.
24
- * 2. CONSENTED — it runs only for a workspace whose own domain we already derived
25
- * at signup; a personal-email signup is `skipped_personal_email`
26
- * upstream and never reaches here, so we never research a stranger.
25
+ * sub-ceiling on the provider (external-money) half. The cap bounds
26
+ * OXYGEN's own money now, and it is the same number.
27
+ * 2. AUTHORIZED — it runs only for a workspace whose creator typed the company
28
+ * website into the REQUIRED field at workspace creation; that
29
+ * entry is recorded as `knowledge_bootstrap_consent` (source
30
+ * `workspace_creation`) and read FAIL-CLOSED, so a workspace that
31
+ * predates the field is never researched and a personal-email
32
+ * signup with no website never reaches the worker. We never
33
+ * research a stranger.
27
34
  * 3. JOURNALLED — the marker below records status, timing, run id, credits and the
28
35
  * domain in `knowledge_state.watermarks`; the URLs read land as
29
36
  * immutable `page_sources` rows plus an `ingest` knowledge_log
@@ -36,33 +43,14 @@
36
43
  * ordinary revisioned wiki writes a human can revert.
37
44
  * 6. EXACTLY-ONCE — the marker is claimed with a conditional UPDATE, so N worker
38
45
  * replicas polling one tenant produce one run, never N runs.
46
+ * 7. OXYGEN-FUNDED — the internal worker scope records nominal managed-credit
47
+ * holds and settlements with zero customer-balance deltas.
48
+ * Manual reruns remain customer-funded. Durable call checkpoints
49
+ * replay completed results and never repeat uncertain dispatch.
39
50
  *
40
- * CAP ARITHMETIC (prices re-read from packages/integrations/src/cogs-rates.ts —
41
- * confirm them there rather than trusting this comment after a re-pricing):
42
- *
43
- * serper.search 5 cr per call (cogs-rates: serper.customerOxygen…search)
44
- * exa.contents 5 cr per CALL (not per URL — this is the whole trick)
45
- * AI column, medium tier 75 cr per run (pricing-sheet: AI_COLUMN_CREDITS.medium)
46
- *
47
- * Happy path = 1 serper.search (5) + 1 exa.contents over <= 5 URLs (5)
48
- * + 1 medium synthesis pass (75) = 85 cr
49
- * Enrichment sub-cap 60 cr — ~6x the 10 cr the happy path needs, so a waterfall
50
- * retry or a second search fits, and a runaway
51
- * per-URL fetch loop is stopped long before the
52
- * overall ceiling.
53
- * Hard cap 300 cr — 85 cr nominal plus room for two retried synthesis
54
- * passes. 3% of the 10,000-credit signup grant
55
- * (FREE_SIGNUP_GRANT_CREDITS, ./billing.ts), so the
56
- * pass can never eat a founder's trial.
57
- *
58
- * The obvious implementation — firecrawl.map + 3x firecrawl.scrape — costs
59
- * 4 x 40 = 160 cr for the SAME job, because Firecrawl bills per page. exa.contents
60
- * is priced per call and is built for fetching text at known URLs; that is why the
61
- * read budget is a page COUNT (`KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ`) inside one
62
- * call rather than a call count.
63
- *
64
- * This module is a leaf: constants and types only, no DB and no provider imports, so
65
- * the worker, tenant-db, CLI, and API surfaces all bind to one contract.
51
+ * New workspace consent scopes the submitted domain and actual signup creator.
52
+ * Professional person evidence remains actor-scoped; ICP/offers are hypotheses.
53
+ * The provider sub-cap covers every external research leg, including fallbacks.
66
54
  */
67
55
  import type { HostedAiLevel } from "./hosted-ai.js";
68
56
  /** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
@@ -127,6 +115,9 @@ export declare function isKnowledgeBootstrapStatus(value: unknown): value is Kno
127
115
  * same thing.
128
116
  */
129
117
  export type KnowledgeBootstrapMarker = {
118
+ /** Versioned claims can recover a crash before the first dispatch checkpoint.
119
+ * Absence identifies legacy attempts whose dispatch history is unknown. */
120
+ execution_version?: 1;
130
121
  status: KnowledgeBootstrapStatus;
131
122
  /** ISO timestamp the claim was won. */
132
123
  started_at: string | null;
@@ -164,9 +155,11 @@ export type KnowledgeBootstrapMarker = {
164
155
  linkedin_url?: string | null;
165
156
  };
166
157
  /**
167
- * Control-DB `organizations.metadata` key carrying the workspace's recorded consent
168
- * to the automatic research pass. Written once at signup by the setup surface; read
169
- * by the worker cycle before anything is spent.
158
+ * Control-DB `organizations.metadata` key carrying the workspace's recorded
159
+ * authorization for the automatic research pass. Written once, when the creator
160
+ * names the company website at workspace creation (`source: "workspace_creation"`;
161
+ * rows written by the retired `/setup` checkbox carry `source: "signup"` and stay
162
+ * valid); read by the worker cycle before anything is spent.
170
163
  *
171
164
  * It lives in shared, not in either caller, so the writer and the reader cannot
172
165
  * drift onto two different key names — a silent drift there would either deny every
@@ -176,9 +169,15 @@ export type KnowledgeBootstrapMarker = {
176
169
  export declare const KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY = "knowledge_bootstrap_consent";
177
170
  /** The recorded consent, exactly as stored under the key above. */
178
171
  export type KnowledgeBootstrapConsent = {
172
+ /** New workspace creation may authorize bounded creator-profile research. */
173
+ scope?: "company_and_creator";
174
+ domain?: string;
179
175
  /** ISO timestamp consent was recorded. Must parse; a marker that does not is not consent. */
180
176
  granted_at: string;
181
- /** Where it came from, e.g. `signup`. Informational. */
177
+ /**
178
+ * Where it came from: `workspace_creation` (the required website field on the
179
+ * create-workspace screen) or the retired `signup` checkbox. Informational.
180
+ */
182
181
  source: string | null;
183
182
  /** Clerk user who accepted, when the surface knows. Informational. */
184
183
  granted_by_clerk_user_id: string | null;
@@ -227,6 +226,14 @@ export declare const KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY = "company_linked
227
226
  * dead and the workspace is recoverable through the product.
228
227
  */
229
228
  export declare const KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS: number;
229
+ /**
230
+ * Idempotency-key prefix for the ledger row that hands an automatic pass's
231
+ * credits back to the workspace (property 7 above). The key is
232
+ * `${prefix}:${run_id}`, so one pass is covered exactly once no matter how many
233
+ * times the worker revisits the outcome, and a `--force` re-run — a different
234
+ * run — is covered only if it was itself automatic.
235
+ */
236
+ export declare const KNOWLEDGE_BOOTSTRAP_COVERAGE_IDEMPOTENCY_PREFIX = "knowledge_bootstrap_coverage";
230
237
  /**
231
238
  * The one wiki page a bootstrap authors. A stable slug is what makes a re-armed pass
232
239
  * REVISE the note rather than litter the wiki with `company-research-2`.
@@ -238,13 +245,14 @@ export declare const KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS: number;
238
245
  */
239
246
  export declare const KNOWLEDGE_BOOTSTRAP_RESEARCH_PAGE_SLUG = "company-research";
240
247
  /**
241
- * The typed `company_profile` sections a bootstrap may write — and the exact set the
242
- * "never clobber a profile a human already filled" gate inspects.
248
+ * The typed `company_profile` sections inspected by the automatic bootstrap's
249
+ * "never clobber a profile a human already filled" gate.
243
250
  *
244
251
  * `gtm_stack` and `custom` are structurally excluded: research of a company's public
245
252
  * web presence says nothing about which tools it has connected here, and a workspace
246
253
  * that filled only those has told us nothing about its company and must still be
247
- * bootstrapped. Same reasoning as the slug above — the preview promises this list.
254
+ * bootstrapped. This is the existing-context guard's list, not a list of writes:
255
+ * current research writes company facts and stores inferred GTM hypotheses separately.
248
256
  */
249
257
  export declare const KNOWLEDGE_BOOTSTRAP_PROFILE_SECTIONS: readonly ["company", "offering", "icp", "market"];
250
258
  /**