@oxygen-agent/cli 1.887.5 → 1.893.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/command-manifest.js +11 -1
- package/dist/index.js +277 -16
- package/dist/skills.js +25 -0
- package/node_modules/@oxygen/shared/dist/billing.d.ts +3 -3
- package/node_modules/@oxygen/shared/dist/billing.js +2 -2
- package/node_modules/@oxygen/shared/dist/capability-discovery.js +3 -3
- package/node_modules/@oxygen/shared/dist/feature-gates.d.ts +5 -0
- package/node_modules/@oxygen/shared/dist/feature-gates.js +6 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/index.js +1 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.d.ts +255 -0
- package/node_modules/@oxygen/shared/dist/knowledge-bootstrap.js +238 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.js +19 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +23 -6
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +438 -29
- package/node_modules/@oxygen/shared/dist/version.d.ts +1 -1
- package/node_modules/@oxygen/shared/dist/version.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge bootstrap — the once-per-workspace, automatic company research pass.
|
|
3
|
+
*
|
|
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.
|
|
12
|
+
*
|
|
13
|
+
* WHY THIS IS NOT A VIOLATION OF THE PAID-ACTIONS RULE. `CLAUDE.md` says never run
|
|
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
|
|
19
|
+
* convenience, and if you remove one, the exception no longer stands:
|
|
20
|
+
*
|
|
21
|
+
* 1. CAPPED — `KNOWLEDGE_BOOTSTRAP_MAX_CREDITS` is a hard ceiling on the whole
|
|
22
|
+
* 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.
|
|
27
|
+
* 3. JOURNALLED — the marker below records status, timing, run id, credits and the
|
|
28
|
+
* domain in `knowledge_state.watermarks`; the URLs read land as
|
|
29
|
+
* immutable `page_sources` rows plus an `ingest` knowledge_log
|
|
30
|
+
* entry. Nothing about the spend is invisible.
|
|
31
|
+
* 4. KILL-SWITCHED — `OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED` stops it fleet-wide at
|
|
32
|
+
* the worker with no deploy, matching the other OXYGEN_KNOWLEDGE_*
|
|
33
|
+
* switches.
|
|
34
|
+
* 5. REVERSIBLE — the marker is a single jsonb key; clearing it (the `--force`
|
|
35
|
+
* path) re-arms the pass, and the pages/profile it wrote are
|
|
36
|
+
* ordinary revisioned wiki writes a human can revert.
|
|
37
|
+
* 6. EXACTLY-ONCE — the marker is claimed with a conditional UPDATE, so N worker
|
|
38
|
+
* replicas polling one tenant produce one run, never N runs.
|
|
39
|
+
*
|
|
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.
|
|
66
|
+
*/
|
|
67
|
+
import type { HostedAiLevel } from "./hosted-ai.js";
|
|
68
|
+
/** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
|
|
69
|
+
export declare const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 300;
|
|
70
|
+
/**
|
|
71
|
+
* Tighter sub-ceiling on the provider/enrichment half (search + page fetch), i.e.
|
|
72
|
+
* the external-money phase. Reaching it degrades the pass to `partial` — synthesize
|
|
73
|
+
* from what was read — rather than failing it.
|
|
74
|
+
*/
|
|
75
|
+
export declare const KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS = 60;
|
|
76
|
+
/**
|
|
77
|
+
* Reasoning tier the synthesis pass runs at. `medium` ("Oxygen Balanced") is the
|
|
78
|
+
* deliberate middle: `low` cannot hold a company profile's structure together, and
|
|
79
|
+
* `high` triples the cost of a pass nobody asked for.
|
|
80
|
+
*/
|
|
81
|
+
export declare const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_TIER: HostedAiLevel;
|
|
82
|
+
/**
|
|
83
|
+
* Most URLs whose text the pass may read — inside ONE `exa.contents` call, which is
|
|
84
|
+
* priced per call. Raising this raises latency and prompt size, not provider spend;
|
|
85
|
+
* it is a quality/latency knob, and the credit ceilings above remain the money guard.
|
|
86
|
+
*/
|
|
87
|
+
export declare const KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ = 5;
|
|
88
|
+
/** Fleet-wide kill switch, matching OXYGEN_KNOWLEDGE_RETRIEVAL/AUTO_SOURCES_DISABLED. */
|
|
89
|
+
export declare const KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR = "OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED";
|
|
90
|
+
/**
|
|
91
|
+
* Is the automatic bootstrap disabled by operator env?
|
|
92
|
+
*
|
|
93
|
+
* Unset = ENABLED (this is a fail-OPEN switch, unlike the free-grant flag): the
|
|
94
|
+
* feature is meant to run for every eligible workspace, and the kill switch exists to
|
|
95
|
+
* stop it fleet-wide in an incident with no deploy. `"1"` or `"true"` disables it;
|
|
96
|
+
* any other value is treated as unset, exactly like the sibling knowledge switches.
|
|
97
|
+
*/
|
|
98
|
+
export declare function knowledgeBootstrapDisabled(env?: Record<string, string | undefined>): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Lifecycle of the once-ever marker.
|
|
101
|
+
*
|
|
102
|
+
* queued — reserved: a surface claimed the slot but has not started work.
|
|
103
|
+
* running — claimed by a worker replica; the pass is in flight.
|
|
104
|
+
* completed — profile + wiki page written.
|
|
105
|
+
* partial — some evidence gathered and written, but a cap, a provider failure, or
|
|
106
|
+
* a thin domain stopped it short. Deliberately TERMINAL.
|
|
107
|
+
* skipped — never attempted for a policy reason (no domain, personal-email
|
|
108
|
+
* signup, kill switch, zero spendable balance). `reason` says which.
|
|
109
|
+
* failed — attempted and errored. Also TERMINAL.
|
|
110
|
+
*
|
|
111
|
+
* Every one of these blocks a second claim: "once per workspace, ever" means the
|
|
112
|
+
* marker's mere EXISTENCE is the lock, not its value. A human re-arms the pass with
|
|
113
|
+
* `--force` (`POST /api/cli/knowledge/bootstrap`, which calls tenant-db's
|
|
114
|
+
* `requeueKnowledgeBootstrap`) — the one deliberate way to spend a second time. It
|
|
115
|
+
* overwrites every status above EXCEPT a `running` marker that is still LIVE, i.e.
|
|
116
|
+
* whose `started_at` is inside {@link KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS}; a `running`
|
|
117
|
+
* marker older than that window is stale and IS re-armable, which is how a wedged pass
|
|
118
|
+
* is recovered without hand-written SQL.
|
|
119
|
+
*/
|
|
120
|
+
export declare const KNOWLEDGE_BOOTSTRAP_STATUSES: readonly ["queued", "running", "completed", "partial", "skipped", "failed"];
|
|
121
|
+
export type KnowledgeBootstrapStatus = (typeof KNOWLEDGE_BOOTSTRAP_STATUSES)[number];
|
|
122
|
+
export declare function isKnowledgeBootstrapStatus(value: unknown): value is KnowledgeBootstrapStatus;
|
|
123
|
+
/**
|
|
124
|
+
* The marker itself, stored VERBATIM at `knowledge_state.watermarks -> 'bootstrap'`.
|
|
125
|
+
* Field names are snake_case because this type IS the on-disk jsonb shape — keeping
|
|
126
|
+
* them identical means a human reading the row and an agent reading the type see the
|
|
127
|
+
* same thing.
|
|
128
|
+
*/
|
|
129
|
+
export type KnowledgeBootstrapMarker = {
|
|
130
|
+
status: KnowledgeBootstrapStatus;
|
|
131
|
+
/** ISO timestamp the claim was won. */
|
|
132
|
+
started_at: string | null;
|
|
133
|
+
/** ISO timestamp a terminal status was written; null while `queued`/`running`. */
|
|
134
|
+
completed_at: string | null;
|
|
135
|
+
/** `ox_runs.runs` id (source_type `system`) carrying the observable run. */
|
|
136
|
+
run_id: string | null;
|
|
137
|
+
/** Managed credits actually spent, across every phase. Never exceeds the cap. */
|
|
138
|
+
credits_used: number;
|
|
139
|
+
/** The workspace domain researched (from organizations.iconDomain). */
|
|
140
|
+
domain: string | null;
|
|
141
|
+
/** Why a pass was skipped/partial/failed — a short machine-ish token, not prose. */
|
|
142
|
+
reason: string | null;
|
|
143
|
+
/** Slugs of the wiki pages this pass created or updated (the URLs it READ are in
|
|
144
|
+
* page_sources — see recordKnowledgeUrlSources). */
|
|
145
|
+
pages: string[];
|
|
146
|
+
/**
|
|
147
|
+
* The credit ceiling the REQUESTER authorized, when a surface reserved this slot
|
|
148
|
+
* (`POST /api/cli/knowledge/bootstrap`, mode `live`, which requires `max_credits`).
|
|
149
|
+
*
|
|
150
|
+
* Absent/null means the automatic pass, which is authorized by signup consent and
|
|
151
|
+
* bounded by `KNOWLEDGE_BOOTSTRAP_MAX_CREDITS` instead. It rides the marker because
|
|
152
|
+
* the surface that takes the authorization is not the process that spends: a
|
|
153
|
+
* request that says "up to 100" and a worker that spends up to 300 is not a capped
|
|
154
|
+
* pass, it is an uncapped one with a friendly form. Always clamped to the platform
|
|
155
|
+
* cap on the way in — a marker can lower the ceiling, never raise it.
|
|
156
|
+
*/
|
|
157
|
+
max_credits?: number | null;
|
|
158
|
+
/**
|
|
159
|
+
* Canonical company LinkedIn URL the requester supplied, when they supplied one.
|
|
160
|
+
* The same hint `/setup` stores at KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY, but
|
|
161
|
+
* carried per-request so a one-off `--linkedin` never mutates the workspace's
|
|
162
|
+
* recorded answer.
|
|
163
|
+
*/
|
|
164
|
+
linkedin_url?: string | null;
|
|
165
|
+
};
|
|
166
|
+
/**
|
|
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.
|
|
170
|
+
*
|
|
171
|
+
* It lives in shared, not in either caller, so the writer and the reader cannot
|
|
172
|
+
* drift onto two different key names — a silent drift there would either deny every
|
|
173
|
+
* workspace its bootstrap forever, or (far worse) make the reader see consent that
|
|
174
|
+
* nobody recorded.
|
|
175
|
+
*/
|
|
176
|
+
export declare const KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY = "knowledge_bootstrap_consent";
|
|
177
|
+
/** The recorded consent, exactly as stored under the key above. */
|
|
178
|
+
export type KnowledgeBootstrapConsent = {
|
|
179
|
+
/** ISO timestamp consent was recorded. Must parse; a marker that does not is not consent. */
|
|
180
|
+
granted_at: string;
|
|
181
|
+
/** Where it came from, e.g. `signup`. Informational. */
|
|
182
|
+
source: string | null;
|
|
183
|
+
/** Clerk user who accepted, when the surface knows. Informational. */
|
|
184
|
+
granted_by_clerk_user_id: string | null;
|
|
185
|
+
};
|
|
186
|
+
/**
|
|
187
|
+
* Read consent out of an organization's control-DB metadata blob, FAIL-CLOSED.
|
|
188
|
+
*
|
|
189
|
+
* Absent, malformed, non-object, or carrying an unparseable `granted_at` all read as
|
|
190
|
+
* "not consented" — a workspace is never researched on the strength of a truthy-looking
|
|
191
|
+
* value. This is deliberately stricter than the kill switch beside it: the switch is
|
|
192
|
+
* fail-OPEN because an operator flips it, while consent is fail-CLOSED because its
|
|
193
|
+
* absence is the default state of every workspace that predates the feature, and
|
|
194
|
+
* spending a stranger's credits on a guess is the one outcome the exception cannot
|
|
195
|
+
* survive.
|
|
196
|
+
*/
|
|
197
|
+
export declare function readKnowledgeBootstrapConsent(metadata: unknown): KnowledgeBootstrapConsent | null;
|
|
198
|
+
/**
|
|
199
|
+
* Control-DB `organizations.metadata` key carrying the company LinkedIn page the
|
|
200
|
+
* workspace typed at signup, when it typed one.
|
|
201
|
+
*
|
|
202
|
+
* It sits beside the consent key for the same reason: the setup surface writes it and
|
|
203
|
+
* the worker reads it, and two spellings of the key would silently drop a hint the
|
|
204
|
+
* customer took the trouble to give us. The stored value is the canonical form
|
|
205
|
+
* produced by `normalizeLinkedinCompanyUrl` (./dnc-identities.js) — normalize on the
|
|
206
|
+
* way in AND on the way out, never trust the raw string, and never let a personal
|
|
207
|
+
* `/in/` profile through: this is the COMPANY page, and researching a founder's
|
|
208
|
+
* personal profile as if it were the company is exactly the confidently-wrong write
|
|
209
|
+
* the bootstrap's domain gate exists to prevent.
|
|
210
|
+
*
|
|
211
|
+
* Optional by design. Its absence is normal and costs nothing: the research pass
|
|
212
|
+
* resolves the company from the domain and pays for a LinkedIn lookup only when it
|
|
213
|
+
* has a page worth looking up.
|
|
214
|
+
*/
|
|
215
|
+
export declare const KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY = "company_linkedin_url";
|
|
216
|
+
/**
|
|
217
|
+
* How long a `running` marker may sit before the workspace is reported as STUCK.
|
|
218
|
+
*
|
|
219
|
+
* Nothing retries it automatically. An unattended retry on a paid path is how a founder
|
|
220
|
+
* gets billed twice for one pass, so a crashed bootstrap stays visibly unfinished until
|
|
221
|
+
* a human re-arms it with `--force` (tenant-db's `requeueKnowledgeBootstrap`).
|
|
222
|
+
*
|
|
223
|
+
* This bound does two things, and the second is load-bearing: it decides when the
|
|
224
|
+
* worker starts SAYING a pass is stuck, AND it is the exact window after which
|
|
225
|
+
* `--force` may re-arm over a `running` marker. Inside the window the re-arm is
|
|
226
|
+
* refused, because the pass may still be spending; outside it the marker is presumed
|
|
227
|
+
* dead and the workspace is recoverable through the product.
|
|
228
|
+
*/
|
|
229
|
+
export declare const KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS: number;
|
|
230
|
+
/**
|
|
231
|
+
* The one wiki page a bootstrap authors. A stable slug is what makes a re-armed pass
|
|
232
|
+
* REVISE the note rather than litter the wiki with `company-research-2`.
|
|
233
|
+
*
|
|
234
|
+
* It lives here rather than in the worker because it is a PROMISE the preview surface
|
|
235
|
+
* makes before anything runs (`POST /api/cli/knowledge/bootstrap` returns it inside
|
|
236
|
+
* `would_write`, with a deep-link). Two copies of a promised slug is how a dry run
|
|
237
|
+
* starts lying about which page it is going to write.
|
|
238
|
+
*/
|
|
239
|
+
export declare const KNOWLEDGE_BOOTSTRAP_RESEARCH_PAGE_SLUG = "company-research";
|
|
240
|
+
/**
|
|
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.
|
|
243
|
+
*
|
|
244
|
+
* `gtm_stack` and `custom` are structurally excluded: research of a company's public
|
|
245
|
+
* web presence says nothing about which tools it has connected here, and a workspace
|
|
246
|
+
* 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.
|
|
248
|
+
*/
|
|
249
|
+
export declare const KNOWLEDGE_BOOTSTRAP_PROFILE_SECTIONS: readonly ["company", "offering", "icp", "market"];
|
|
250
|
+
/**
|
|
251
|
+
* Mirrors the four `CONTEXT_PROFILE_SECTIONS` members above. Declared locally because
|
|
252
|
+
* this module is a leaf (no tenant-db import); the union stays assignable to
|
|
253
|
+
* `ContextProfileSection`, so the worker keeps its real type with no cast.
|
|
254
|
+
*/
|
|
255
|
+
export type KnowledgeBootstrapProfileSection = "company" | "offering" | "icp" | "market";
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Knowledge bootstrap — the once-per-workspace, automatic company research pass.
|
|
3
|
+
*
|
|
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.
|
|
12
|
+
*
|
|
13
|
+
* WHY THIS IS NOT A VIOLATION OF THE PAID-ACTIONS RULE. `CLAUDE.md` says never run
|
|
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
|
|
19
|
+
* convenience, and if you remove one, the exception no longer stands:
|
|
20
|
+
*
|
|
21
|
+
* 1. CAPPED — `KNOWLEDGE_BOOTSTRAP_MAX_CREDITS` is a hard ceiling on the whole
|
|
22
|
+
* 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.
|
|
27
|
+
* 3. JOURNALLED — the marker below records status, timing, run id, credits and the
|
|
28
|
+
* domain in `knowledge_state.watermarks`; the URLs read land as
|
|
29
|
+
* immutable `page_sources` rows plus an `ingest` knowledge_log
|
|
30
|
+
* entry. Nothing about the spend is invisible.
|
|
31
|
+
* 4. KILL-SWITCHED — `OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED` stops it fleet-wide at
|
|
32
|
+
* the worker with no deploy, matching the other OXYGEN_KNOWLEDGE_*
|
|
33
|
+
* switches.
|
|
34
|
+
* 5. REVERSIBLE — the marker is a single jsonb key; clearing it (the `--force`
|
|
35
|
+
* path) re-arms the pass, and the pages/profile it wrote are
|
|
36
|
+
* ordinary revisioned wiki writes a human can revert.
|
|
37
|
+
* 6. EXACTLY-ONCE — the marker is claimed with a conditional UPDATE, so N worker
|
|
38
|
+
* replicas polling one tenant produce one run, never N runs.
|
|
39
|
+
*
|
|
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.
|
|
66
|
+
*/
|
|
67
|
+
/** Hard ceiling on managed credits ONE workspace bootstrap may spend, all phases. */
|
|
68
|
+
export const KNOWLEDGE_BOOTSTRAP_MAX_CREDITS = 300;
|
|
69
|
+
/**
|
|
70
|
+
* Tighter sub-ceiling on the provider/enrichment half (search + page fetch), i.e.
|
|
71
|
+
* the external-money phase. Reaching it degrades the pass to `partial` — synthesize
|
|
72
|
+
* from what was read — rather than failing it.
|
|
73
|
+
*/
|
|
74
|
+
export const KNOWLEDGE_BOOTSTRAP_ENRICHMENT_MAX_CREDITS = 60;
|
|
75
|
+
/**
|
|
76
|
+
* Reasoning tier the synthesis pass runs at. `medium` ("Oxygen Balanced") is the
|
|
77
|
+
* deliberate middle: `low` cannot hold a company profile's structure together, and
|
|
78
|
+
* `high` triples the cost of a pass nobody asked for.
|
|
79
|
+
*/
|
|
80
|
+
export const KNOWLEDGE_BOOTSTRAP_SYNTHESIS_TIER = "medium";
|
|
81
|
+
/**
|
|
82
|
+
* Most URLs whose text the pass may read — inside ONE `exa.contents` call, which is
|
|
83
|
+
* priced per call. Raising this raises latency and prompt size, not provider spend;
|
|
84
|
+
* it is a quality/latency knob, and the credit ceilings above remain the money guard.
|
|
85
|
+
*/
|
|
86
|
+
export const KNOWLEDGE_BOOTSTRAP_MAX_PAGES_READ = 5;
|
|
87
|
+
/** Fleet-wide kill switch, matching OXYGEN_KNOWLEDGE_RETRIEVAL/AUTO_SOURCES_DISABLED. */
|
|
88
|
+
export const KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR = "OXYGEN_KNOWLEDGE_BOOTSTRAP_DISABLED";
|
|
89
|
+
/**
|
|
90
|
+
* Is the automatic bootstrap disabled by operator env?
|
|
91
|
+
*
|
|
92
|
+
* Unset = ENABLED (this is a fail-OPEN switch, unlike the free-grant flag): the
|
|
93
|
+
* feature is meant to run for every eligible workspace, and the kill switch exists to
|
|
94
|
+
* stop it fleet-wide in an incident with no deploy. `"1"` or `"true"` disables it;
|
|
95
|
+
* any other value is treated as unset, exactly like the sibling knowledge switches.
|
|
96
|
+
*/
|
|
97
|
+
export function knowledgeBootstrapDisabled(env) {
|
|
98
|
+
const value = (env ?? process.env)[KNOWLEDGE_BOOTSTRAP_DISABLED_ENV_VAR];
|
|
99
|
+
return value === "1" || value === "true";
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Lifecycle of the once-ever marker.
|
|
103
|
+
*
|
|
104
|
+
* queued — reserved: a surface claimed the slot but has not started work.
|
|
105
|
+
* running — claimed by a worker replica; the pass is in flight.
|
|
106
|
+
* completed — profile + wiki page written.
|
|
107
|
+
* partial — some evidence gathered and written, but a cap, a provider failure, or
|
|
108
|
+
* a thin domain stopped it short. Deliberately TERMINAL.
|
|
109
|
+
* skipped — never attempted for a policy reason (no domain, personal-email
|
|
110
|
+
* signup, kill switch, zero spendable balance). `reason` says which.
|
|
111
|
+
* failed — attempted and errored. Also TERMINAL.
|
|
112
|
+
*
|
|
113
|
+
* Every one of these blocks a second claim: "once per workspace, ever" means the
|
|
114
|
+
* marker's mere EXISTENCE is the lock, not its value. A human re-arms the pass with
|
|
115
|
+
* `--force` (`POST /api/cli/knowledge/bootstrap`, which calls tenant-db's
|
|
116
|
+
* `requeueKnowledgeBootstrap`) — the one deliberate way to spend a second time. It
|
|
117
|
+
* overwrites every status above EXCEPT a `running` marker that is still LIVE, i.e.
|
|
118
|
+
* whose `started_at` is inside {@link KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS}; a `running`
|
|
119
|
+
* marker older than that window is stale and IS re-armable, which is how a wedged pass
|
|
120
|
+
* is recovered without hand-written SQL.
|
|
121
|
+
*/
|
|
122
|
+
export const KNOWLEDGE_BOOTSTRAP_STATUSES = [
|
|
123
|
+
"queued",
|
|
124
|
+
"running",
|
|
125
|
+
"completed",
|
|
126
|
+
"partial",
|
|
127
|
+
"skipped",
|
|
128
|
+
"failed",
|
|
129
|
+
];
|
|
130
|
+
export function isKnowledgeBootstrapStatus(value) {
|
|
131
|
+
return (typeof value === "string" &&
|
|
132
|
+
KNOWLEDGE_BOOTSTRAP_STATUSES.includes(value));
|
|
133
|
+
}
|
|
134
|
+
// ---------------------------------------------------------------------------
|
|
135
|
+
// Consent (property 2 of the exception above, made explicit)
|
|
136
|
+
// ---------------------------------------------------------------------------
|
|
137
|
+
/**
|
|
138
|
+
* Control-DB `organizations.metadata` key carrying the workspace's recorded consent
|
|
139
|
+
* to the automatic research pass. Written once at signup by the setup surface; read
|
|
140
|
+
* by the worker cycle before anything is spent.
|
|
141
|
+
*
|
|
142
|
+
* It lives in shared, not in either caller, so the writer and the reader cannot
|
|
143
|
+
* drift onto two different key names — a silent drift there would either deny every
|
|
144
|
+
* workspace its bootstrap forever, or (far worse) make the reader see consent that
|
|
145
|
+
* nobody recorded.
|
|
146
|
+
*/
|
|
147
|
+
export const KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY = "knowledge_bootstrap_consent";
|
|
148
|
+
/**
|
|
149
|
+
* Read consent out of an organization's control-DB metadata blob, FAIL-CLOSED.
|
|
150
|
+
*
|
|
151
|
+
* Absent, malformed, non-object, or carrying an unparseable `granted_at` all read as
|
|
152
|
+
* "not consented" — a workspace is never researched on the strength of a truthy-looking
|
|
153
|
+
* value. This is deliberately stricter than the kill switch beside it: the switch is
|
|
154
|
+
* fail-OPEN because an operator flips it, while consent is fail-CLOSED because its
|
|
155
|
+
* absence is the default state of every workspace that predates the feature, and
|
|
156
|
+
* spending a stranger's credits on a guess is the one outcome the exception cannot
|
|
157
|
+
* survive.
|
|
158
|
+
*/
|
|
159
|
+
export function readKnowledgeBootstrapConsent(metadata) {
|
|
160
|
+
if (!metadata || typeof metadata !== "object" || Array.isArray(metadata))
|
|
161
|
+
return null;
|
|
162
|
+
const raw = metadata[KNOWLEDGE_BOOTSTRAP_CONSENT_METADATA_KEY];
|
|
163
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw))
|
|
164
|
+
return null;
|
|
165
|
+
const record = raw;
|
|
166
|
+
const grantedAt = record.granted_at;
|
|
167
|
+
if (typeof grantedAt !== "string" || grantedAt.length === 0)
|
|
168
|
+
return null;
|
|
169
|
+
if (!Number.isFinite(Date.parse(grantedAt)))
|
|
170
|
+
return null;
|
|
171
|
+
return {
|
|
172
|
+
granted_at: grantedAt,
|
|
173
|
+
source: typeof record.source === "string" && record.source.length > 0 ? record.source : null,
|
|
174
|
+
granted_by_clerk_user_id: typeof record.granted_by_clerk_user_id === "string" && record.granted_by_clerk_user_id.length > 0
|
|
175
|
+
? record.granted_by_clerk_user_id
|
|
176
|
+
: null,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Control-DB `organizations.metadata` key carrying the company LinkedIn page the
|
|
181
|
+
* workspace typed at signup, when it typed one.
|
|
182
|
+
*
|
|
183
|
+
* It sits beside the consent key for the same reason: the setup surface writes it and
|
|
184
|
+
* the worker reads it, and two spellings of the key would silently drop a hint the
|
|
185
|
+
* customer took the trouble to give us. The stored value is the canonical form
|
|
186
|
+
* produced by `normalizeLinkedinCompanyUrl` (./dnc-identities.js) — normalize on the
|
|
187
|
+
* way in AND on the way out, never trust the raw string, and never let a personal
|
|
188
|
+
* `/in/` profile through: this is the COMPANY page, and researching a founder's
|
|
189
|
+
* personal profile as if it were the company is exactly the confidently-wrong write
|
|
190
|
+
* the bootstrap's domain gate exists to prevent.
|
|
191
|
+
*
|
|
192
|
+
* Optional by design. Its absence is normal and costs nothing: the research pass
|
|
193
|
+
* resolves the company from the domain and pays for a LinkedIn lookup only when it
|
|
194
|
+
* has a page worth looking up.
|
|
195
|
+
*/
|
|
196
|
+
export const KNOWLEDGE_BOOTSTRAP_LINKEDIN_METADATA_KEY = "company_linkedin_url";
|
|
197
|
+
/**
|
|
198
|
+
* How long a `running` marker may sit before the workspace is reported as STUCK.
|
|
199
|
+
*
|
|
200
|
+
* Nothing retries it automatically. An unattended retry on a paid path is how a founder
|
|
201
|
+
* gets billed twice for one pass, so a crashed bootstrap stays visibly unfinished until
|
|
202
|
+
* a human re-arms it with `--force` (tenant-db's `requeueKnowledgeBootstrap`).
|
|
203
|
+
*
|
|
204
|
+
* This bound does two things, and the second is load-bearing: it decides when the
|
|
205
|
+
* worker starts SAYING a pass is stuck, AND it is the exact window after which
|
|
206
|
+
* `--force` may re-arm over a `running` marker. Inside the window the re-arm is
|
|
207
|
+
* refused, because the pass may still be spending; outside it the marker is presumed
|
|
208
|
+
* dead and the workspace is recoverable through the product.
|
|
209
|
+
*/
|
|
210
|
+
export const KNOWLEDGE_BOOTSTRAP_STUCK_AFTER_MS = 60 * 60 * 1000;
|
|
211
|
+
// ---------------------------------------------------------------------------
|
|
212
|
+
// Write targets (what a bootstrap actually changes)
|
|
213
|
+
// ---------------------------------------------------------------------------
|
|
214
|
+
/**
|
|
215
|
+
* The one wiki page a bootstrap authors. A stable slug is what makes a re-armed pass
|
|
216
|
+
* REVISE the note rather than litter the wiki with `company-research-2`.
|
|
217
|
+
*
|
|
218
|
+
* It lives here rather than in the worker because it is a PROMISE the preview surface
|
|
219
|
+
* makes before anything runs (`POST /api/cli/knowledge/bootstrap` returns it inside
|
|
220
|
+
* `would_write`, with a deep-link). Two copies of a promised slug is how a dry run
|
|
221
|
+
* starts lying about which page it is going to write.
|
|
222
|
+
*/
|
|
223
|
+
export const KNOWLEDGE_BOOTSTRAP_RESEARCH_PAGE_SLUG = "company-research";
|
|
224
|
+
/**
|
|
225
|
+
* The typed `company_profile` sections a bootstrap may write — and the exact set the
|
|
226
|
+
* "never clobber a profile a human already filled" gate inspects.
|
|
227
|
+
*
|
|
228
|
+
* `gtm_stack` and `custom` are structurally excluded: research of a company's public
|
|
229
|
+
* web presence says nothing about which tools it has connected here, and a workspace
|
|
230
|
+
* that filled only those has told us nothing about its company and must still be
|
|
231
|
+
* bootstrapped. Same reasoning as the slug above — the preview promises this list.
|
|
232
|
+
*/
|
|
233
|
+
export const KNOWLEDGE_BOOTSTRAP_PROFILE_SECTIONS = [
|
|
234
|
+
"company",
|
|
235
|
+
"offering",
|
|
236
|
+
"icp",
|
|
237
|
+
"market",
|
|
238
|
+
];
|
|
@@ -37,4 +37,7 @@ export declare const KNOWLEDGE_GRAPH_MAX_NODES = 2000;
|
|
|
37
37
|
export declare const KNOWLEDGE_GRAPH_DEFAULT_DEPTH = 1;
|
|
38
38
|
export declare const KNOWLEDGE_GRAPH_MAX_DEPTH = 3;
|
|
39
39
|
export declare const KNOWLEDGE_SYNC_MAX_PAGE_BATCH = 200;
|
|
40
|
+
export declare const KNOWLEDGE_STUB_MARKER = "Stub \u2014 fill me.";
|
|
41
|
+
export declare const KNOWLEDGE_BOOTSTRAP_COMMAND = "oxygen knowledge bootstrap";
|
|
42
|
+
export declare const KNOWLEDGE_BOOTSTRAP_AVAILABLE = true;
|
|
40
43
|
export declare const RESERVED_KNOWLEDGE_FRONTMATTER_KEYS: readonly ["oxygen_page", "id", "slug", "type", "title", "status", "tags", "canonical", "summary", "revision", "updated_at", "web_url"];
|
|
@@ -123,6 +123,25 @@ export const KNOWLEDGE_GRAPH_MAX_NODES = 2_000;
|
|
|
123
123
|
export const KNOWLEDGE_GRAPH_DEFAULT_DEPTH = 1;
|
|
124
124
|
export const KNOWLEDGE_GRAPH_MAX_DEPTH = 3;
|
|
125
125
|
export const KNOWLEDGE_SYNC_MAX_PAGE_BATCH = 200;
|
|
126
|
+
// The domain-grounded scaffold-fill command. KNOWLEDGE_BOOTSTRAP_AVAILABLE is the
|
|
127
|
+
// single gate: while it is false, `oxygen knowledge lint` reports researchable stubs
|
|
128
|
+
// as FACTS (which stubs are still empty, and whether the workspace has a domain a
|
|
129
|
+
// research pass could use) but never names a command that would 404 — the repo's
|
|
130
|
+
// "no placeholder commands" rule. The bootstrap slice flips this one constant and
|
|
131
|
+
// every surface lights up unchanged. NOT dead code: do not delete it in a cleanup pass.
|
|
132
|
+
// The literal line every seeded stub body opens with. It is load-bearing in TWO places,
|
|
133
|
+
// which is why it lives here rather than being inlined:
|
|
134
|
+
// - retrieval (knowledge-retrieval.ts) refuses any page still carrying it, so
|
|
135
|
+
// scaffolding text can never reach a customer's cold email;
|
|
136
|
+
// - lint (knowledge-lint.ts) counts such a page as an unfilled stub.
|
|
137
|
+
// Both used to key off `revision = 1` instead, which a status-only flip defeats:
|
|
138
|
+
// `page upsert <slug> --status active` bumps the revision without touching the body, so
|
|
139
|
+
// the page became simultaneously RETRIEVABLE and invisible to lint — the one state a
|
|
140
|
+
// draft stub exists to prevent. The marker survives that flip; the revision does not.
|
|
141
|
+
// Changing this string means changing every seeded body in knowledge-seed-content.ts.
|
|
142
|
+
export const KNOWLEDGE_STUB_MARKER = "Stub — fill me.";
|
|
143
|
+
export const KNOWLEDGE_BOOTSTRAP_COMMAND = "oxygen knowledge bootstrap";
|
|
144
|
+
export const KNOWLEDGE_BOOTSTRAP_AVAILABLE = true;
|
|
126
145
|
// Frontmatter keys the deterministic renderer owns; user `data` keys colliding
|
|
127
146
|
// with these are skipped at render time (the stored jsonb keeps them).
|
|
128
147
|
export const RESERVED_KNOWLEDGE_FRONTMATTER_KEYS = [
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { KnowledgePageType, KnowledgePageStatus } from "./knowledge-constants.js";
|
|
2
2
|
/** Monotonic scaffold cursor stored in knowledge_state.scaffold_version. Bump to ship a
|
|
3
|
-
* new wave of starter pages (tag them with a higher minScaffoldVersion).
|
|
4
|
-
|
|
3
|
+
* new wave of starter pages (tag them with a higher minScaffoldVersion).
|
|
4
|
+
* v1: the 9-page core. v2: twelve GTM stubs (voice/brand/messaging/objections, three
|
|
5
|
+
* motion playbooks + qualification, and the personas/customers/research/metrics hubs). */
|
|
6
|
+
export declare const KNOWLEDGE_SCAFFOLD_VERSION = 2;
|
|
5
7
|
export type KnowledgeScaffoldPage = {
|
|
6
8
|
/** Immutable slug; must be valid kebab grammar and NOT a reserved slug. */
|
|
7
9
|
slug: string;
|
|
@@ -14,11 +16,26 @@ export type KnowledgeScaffoldPage = {
|
|
|
14
16
|
body: string;
|
|
15
17
|
/** The first scaffold version that includes this page — seeded when current < this <= target. */
|
|
16
18
|
minScaffoldVersion: number;
|
|
19
|
+
/** True when a research pass over the company's PUBLIC web presence could draft a real
|
|
20
|
+
* first version of this page (what they sell, who they say they sell to, how they
|
|
21
|
+
* sound, who bought). Internal-only pages (playbooks, qualification, metrics, research
|
|
22
|
+
* notes) are never researchable — the public web does not know them. Consumed by
|
|
23
|
+
* `oxygen knowledge lint`'s researchable check. */
|
|
24
|
+
researchable?: boolean;
|
|
17
25
|
};
|
|
18
26
|
/**
|
|
19
|
-
* The
|
|
20
|
-
* page), then
|
|
21
|
-
*
|
|
22
|
-
*
|
|
27
|
+
* The 21-page starter pack. Two active nav pages first (start-here + the conventions
|
|
28
|
+
* schema page), then nineteen draft stub hubs (tag `seed`) — seven from wave 1 and
|
|
29
|
+
* twelve from wave 2. `positioning` seeds as a NON-canonical draft so it writes freely;
|
|
30
|
+
* pinning canonical positioning stays a separate human-approved act. Wave 2 adds ZERO
|
|
31
|
+
* active pages, per the binding rule at the top of this file.
|
|
23
32
|
*/
|
|
24
33
|
export declare const KNOWLEDGE_SEED_PAGES: readonly KnowledgeScaffoldPage[];
|
|
34
|
+
/**
|
|
35
|
+
* Seed slugs a domain-grounded research pass could genuinely draft from the company's
|
|
36
|
+
* PUBLIC web presence — what they sell, who they say they sell to, how they sound, who
|
|
37
|
+
* bought. Derived from the manifest's `researchable` flag (never a second hand-kept list),
|
|
38
|
+
* and consumed by `oxygen knowledge lint`'s researchable check, which reports the ones
|
|
39
|
+
* still sitting unfilled at revision 1 on a workspace that has a company domain.
|
|
40
|
+
*/
|
|
41
|
+
export declare const KNOWLEDGE_RESEARCHABLE_SEED_SLUGS: readonly string[];
|