@zeyos/client 0.3.0 → 0.5.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/CHANGELOG.md +54 -0
- package/README.md +41 -2
- package/agents/README.md +10 -0
- package/agents/shared/zeyos-agent-operating-guide.md +42 -0
- package/agents/shared/zeyos-entity-map.md +5 -1
- package/agents/shared/zeyos-entity-reference.md +89 -33
- package/agents/shared/zeyos-query-patterns.md +26 -0
- package/agents/zeyos-calendar-and-scheduling/SKILL.md +45 -0
- package/agents/zeyos-calendar-and-scheduling/references/workflows.md +49 -0
- package/agents/zeyos-data-quality-and-governance/SKILL.md +43 -0
- package/agents/zeyos-data-quality-and-governance/references/workflows.md +51 -0
- package/agents/zeyos-document-and-approval/SKILL.md +41 -0
- package/agents/zeyos-document-and-approval/references/workflows.md +43 -0
- package/agents/zeyos-procurement-and-supplier-performance/SKILL.md +36 -0
- package/agents/zeyos-procurement-and-supplier-performance/references/workflows.md +46 -0
- package/agents/zeyos-time-tracking/SKILL.md +2 -0
- package/agents/zeyos-time-tracking/references/workflows.md +68 -0
- package/agents/zeyos-work-management/SKILL.md +4 -3
- package/agents/zeyos-work-management/references/workflows.md +39 -1
- package/docs/03-cli/02-commands.md +36 -2
- package/docs/03-cli/03-configuration.md +1 -0
- package/docs/06-okf/01-overview.md +70 -0
- package/docs/06-okf/02-producing-and-consuming.md +46 -0
- package/docs/06-okf/03-keeping-fresh.md +53 -0
- package/docs/06-okf/04-loops.md +58 -0
- package/docs/06-okf/_category_.json +9 -0
- package/okf/concepts/calendar-timezones.md +10 -0
- package/okf/concepts/confirmation-and-side-effects.md +14 -0
- package/okf/concepts/counting-and-sums.md +10 -0
- package/okf/concepts/currency-and-rounding.md +10 -0
- package/okf/concepts/dates-unix-seconds.md +12 -0
- package/okf/concepts/enums.md +14 -0
- package/okf/concepts/filters-vs-filter.md +14 -0
- package/okf/concepts/idempotency-and-deduplication.md +10 -0
- package/okf/concepts/index.md +16 -0
- package/okf/concepts/null-empty-missing.md +10 -0
- package/okf/concepts/official-versus-latest.md +10 -0
- package/okf/concepts/operationid-vocabulary.md +17 -0
- package/okf/concepts/ownership-versus-attention.md +15 -0
- package/okf/concepts/untrusted-business-content.md +10 -0
- package/okf/concepts/visibility-column.md +13 -0
- package/okf/entities/accounts.md +82 -0
- package/okf/entities/actionsteps.md +84 -0
- package/okf/entities/addresses.md +50 -0
- package/okf/entities/applicationassets.md +43 -0
- package/okf/entities/applications.md +62 -0
- package/okf/entities/appointments.md +79 -0
- package/okf/entities/associations.md +41 -0
- package/okf/entities/binfiles.md +32 -0
- package/okf/entities/campaigns.md +66 -0
- package/okf/entities/categories.md +55 -0
- package/okf/entities/channels.md +54 -0
- package/okf/entities/comments.md +44 -0
- package/okf/entities/components.md +46 -0
- package/okf/entities/contacts.md +96 -0
- package/okf/entities/contacts2contacts.md +42 -0
- package/okf/entities/contracts.md +83 -0
- package/okf/entities/couponcodes.md +58 -0
- package/okf/entities/coupons.md +69 -0
- package/okf/entities/customfields.md +59 -0
- package/okf/entities/davservers.md +74 -0
- package/okf/entities/devices.md +65 -0
- package/okf/entities/documents.md +76 -0
- package/okf/entities/dunning.md +82 -0
- package/okf/entities/dunning2transactions.md +46 -0
- package/okf/entities/entities2channels.md +42 -0
- package/okf/entities/events.md +57 -0
- package/okf/entities/feedservers.md +67 -0
- package/okf/entities/files.md +50 -0
- package/okf/entities/follows.md +40 -0
- package/okf/entities/forks.md +54 -0
- package/okf/entities/groups.md +48 -0
- package/okf/entities/groups2users.md +44 -0
- package/okf/entities/index.md +93 -0
- package/okf/entities/invitations.md +53 -0
- package/okf/entities/items.md +95 -0
- package/okf/entities/ledgers.md +56 -0
- package/okf/entities/likes.md +40 -0
- package/okf/entities/links.md +70 -0
- package/okf/entities/mailinglists.md +67 -0
- package/okf/entities/mailingrecipients.md +45 -0
- package/okf/entities/mailservers.md +77 -0
- package/okf/entities/messagereads.md +40 -0
- package/okf/entities/messages.md +104 -0
- package/okf/entities/notes.md +73 -0
- package/okf/entities/objects.md +70 -0
- package/okf/entities/opportunities.md +87 -0
- package/okf/entities/participants.md +52 -0
- package/okf/entities/payments.md +76 -0
- package/okf/entities/permissions.md +46 -0
- package/okf/entities/pricelists.md +70 -0
- package/okf/entities/pricelists2accounts.md +46 -0
- package/okf/entities/prices.md +49 -0
- package/okf/entities/projects.md +72 -0
- package/okf/entities/records.md +75 -0
- package/okf/entities/relateditems.md +43 -0
- package/okf/entities/resources.md +55 -0
- package/okf/entities/services.md +64 -0
- package/okf/entities/stocktransactions.md +72 -0
- package/okf/entities/storages.md +56 -0
- package/okf/entities/suppliers.md +51 -0
- package/okf/entities/tasks.md +86 -0
- package/okf/entities/tickets.md +86 -0
- package/okf/entities/transactions.md +118 -0
- package/okf/entities/users.md +66 -0
- package/okf/entities/weblets.md +66 -0
- package/okf/index.md +11 -0
- package/okf/log.md +4 -0
- package/okf/metrics/account-address-completeness.md +10 -0
- package/okf/metrics/cash-received.md +10 -0
- package/okf/metrics/index.md +9 -0
- package/okf/metrics/invoiced-net-revenue.md +16 -0
- package/okf/metrics/open-customers.md +14 -0
- package/okf/metrics/overdue-receivables.md +12 -0
- package/okf/metrics/stock-movement-by-storage.md +10 -0
- package/okf/metrics/supplier-delivery-performance.md +10 -0
- package/okf/playbooks/activity-timeline.md +11 -0
- package/okf/playbooks/calendar-availability.md +11 -0
- package/okf/playbooks/campaign-recipient-coverage.md +12 -0
- package/okf/playbooks/customer-360.md +12 -0
- package/okf/playbooks/document-approval.md +10 -0
- package/okf/playbooks/duplicate-account-review.md +11 -0
- package/okf/playbooks/effective-customer-price.md +11 -0
- package/okf/playbooks/index.md +13 -0
- package/okf/playbooks/missing-billing-addresses.md +12 -0
- package/okf/playbooks/revenue-this-year.md +19 -0
- package/okf/playbooks/supplier-scorecard.md +10 -0
- package/okf/playbooks/ticket-work-packet.md +11 -0
- package/package.json +11 -3
- package/scripts/data/okf-curation.mjs +446 -0
- package/scripts/generate-client.mjs +4 -275
- package/scripts/generate-okf.mjs +241 -0
- package/scripts/lib/live-test-config.mjs +20 -0
- package/scripts/lib/okf.mjs +272 -0
- package/scripts/lib/spec-model.mjs +325 -0
- package/src/index.js +4 -0
- package/src/runtime/okf.js +237 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import { mkdir,
|
|
1
|
+
import { mkdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
import { buildSchema, buildServices, loadSpec, loadOptionalSpec } from './lib/spec-model.mjs';
|
|
6
6
|
|
|
7
7
|
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
8
|
const SOURCES = [
|
|
@@ -11,230 +11,6 @@ const SOURCES = [
|
|
|
11
11
|
{ service: 'legacyAuth', file: 'openapi/auth.json' }
|
|
12
12
|
];
|
|
13
13
|
|
|
14
|
-
function unescapePointerToken(token) {
|
|
15
|
-
return token.replace(/~1/g, '/').replace(/~0/g, '~');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function pointerGet(doc, ref) {
|
|
19
|
-
if (!ref.startsWith('#/')) {
|
|
20
|
-
throw new Error(`External $ref is not supported: ${ref}`);
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
const parts = ref.slice(2).split('/').map(unescapePointerToken);
|
|
24
|
-
let current = doc;
|
|
25
|
-
for (const part of parts) {
|
|
26
|
-
if (current == null || !Object.prototype.hasOwnProperty.call(current, part)) {
|
|
27
|
-
throw new Error(`Unresolvable $ref: ${ref}`);
|
|
28
|
-
}
|
|
29
|
-
current = current[part];
|
|
30
|
-
}
|
|
31
|
-
return current;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function resolveRef(doc, value) {
|
|
35
|
-
if (!value || typeof value !== 'object') {
|
|
36
|
-
return value;
|
|
37
|
-
}
|
|
38
|
-
if (typeof value.$ref === 'string') {
|
|
39
|
-
return pointerGet(doc, value.$ref);
|
|
40
|
-
}
|
|
41
|
-
return value;
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function normalizeServer(server) {
|
|
45
|
-
const urlTemplate = server?.url || '';
|
|
46
|
-
const defaultVariables = {};
|
|
47
|
-
|
|
48
|
-
if (server?.variables && typeof server.variables === 'object') {
|
|
49
|
-
for (const [name, variable] of Object.entries(server.variables)) {
|
|
50
|
-
if (variable && typeof variable.default === 'string') {
|
|
51
|
-
defaultVariables[name] = variable.default;
|
|
52
|
-
}
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const basePathTemplate = (() => {
|
|
57
|
-
const match = urlTemplate.match(/^https?:\/\/[^/]+(\/.*)$/i);
|
|
58
|
-
return match ? match[1] : '';
|
|
59
|
-
})();
|
|
60
|
-
|
|
61
|
-
return {
|
|
62
|
-
urlTemplate,
|
|
63
|
-
basePathTemplate,
|
|
64
|
-
defaultVariables
|
|
65
|
-
};
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
function normalizeParameters(doc, pathItem, operation) {
|
|
69
|
-
const combined = [
|
|
70
|
-
...(Array.isArray(pathItem?.parameters) ? pathItem.parameters : []),
|
|
71
|
-
...(Array.isArray(operation?.parameters) ? operation.parameters : [])
|
|
72
|
-
];
|
|
73
|
-
|
|
74
|
-
const indexed = new Map();
|
|
75
|
-
for (const rawParameter of combined) {
|
|
76
|
-
const parameter = resolveRef(doc, rawParameter);
|
|
77
|
-
if (!parameter || typeof parameter !== 'object') {
|
|
78
|
-
continue;
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
const key = `${parameter.in || 'unknown'}:${parameter.name || 'unknown'}`;
|
|
82
|
-
indexed.set(key, {
|
|
83
|
-
name: parameter.name,
|
|
84
|
-
in: parameter.in,
|
|
85
|
-
required: Boolean(parameter.required)
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const normalized = Array.from(indexed.values()).filter((parameter) => parameter.name && parameter.in);
|
|
90
|
-
|
|
91
|
-
return {
|
|
92
|
-
all: normalized,
|
|
93
|
-
path: normalized.filter((parameter) => parameter.in === 'path').map((parameter) => parameter.name),
|
|
94
|
-
query: normalized.filter((parameter) => parameter.in === 'query').map((parameter) => parameter.name),
|
|
95
|
-
header: normalized.filter((parameter) => parameter.in === 'header').map((parameter) => parameter.name)
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function normalizeRequestBody(doc, operation) {
|
|
100
|
-
const requestBody = resolveRef(doc, operation?.requestBody);
|
|
101
|
-
if (!requestBody || typeof requestBody !== 'object') {
|
|
102
|
-
return {
|
|
103
|
-
required: false,
|
|
104
|
-
contentTypes: []
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const content = requestBody.content && typeof requestBody.content === 'object' ? requestBody.content : {};
|
|
109
|
-
const contentTypes = Object.keys(content);
|
|
110
|
-
|
|
111
|
-
return {
|
|
112
|
-
required: Boolean(requestBody.required),
|
|
113
|
-
contentTypes
|
|
114
|
-
};
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
function normalizeOperationSecurity(doc, operation) {
|
|
118
|
-
if (Array.isArray(operation?.security)) {
|
|
119
|
-
return operation.security;
|
|
120
|
-
}
|
|
121
|
-
if (Array.isArray(doc.security)) {
|
|
122
|
-
return doc.security;
|
|
123
|
-
}
|
|
124
|
-
return [];
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
function buildOperationId({ operationId, method, route, existingIds }) {
|
|
128
|
-
if (operationId && !existingIds.has(operationId)) {
|
|
129
|
-
return operationId;
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
const routeToken = route
|
|
133
|
-
.replace(/[^a-zA-Z0-9{}]/g, '_')
|
|
134
|
-
.replace(/[{}]/g, '')
|
|
135
|
-
.replace(/_+/g, '_')
|
|
136
|
-
.replace(/^_|_$/g, '');
|
|
137
|
-
|
|
138
|
-
let candidate = operationId || `${method.toLowerCase()}_${routeToken || 'operation'}`;
|
|
139
|
-
let suffix = 1;
|
|
140
|
-
|
|
141
|
-
while (existingIds.has(candidate)) {
|
|
142
|
-
suffix += 1;
|
|
143
|
-
candidate = `${operationId || `${method.toLowerCase()}_${routeToken || 'operation'}`}_${suffix}`;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return candidate;
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
function collectOperations(doc) {
|
|
150
|
-
const operations = [];
|
|
151
|
-
const existingIds = new Set();
|
|
152
|
-
|
|
153
|
-
for (const [route, pathItem] of Object.entries(doc.paths || {})) {
|
|
154
|
-
for (const [method, operation] of Object.entries(pathItem || {})) {
|
|
155
|
-
if (!HTTP_METHODS.has(method)) {
|
|
156
|
-
continue;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
const normalizedMethod = method.toUpperCase();
|
|
160
|
-
const parameters = normalizeParameters(doc, pathItem, operation);
|
|
161
|
-
const requestBody = normalizeRequestBody(doc, operation);
|
|
162
|
-
const security = normalizeOperationSecurity(doc, operation);
|
|
163
|
-
const finalOperationId = buildOperationId({
|
|
164
|
-
operationId: operation.operationId,
|
|
165
|
-
method: normalizedMethod,
|
|
166
|
-
route,
|
|
167
|
-
existingIds
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
existingIds.add(finalOperationId);
|
|
171
|
-
|
|
172
|
-
operations.push({
|
|
173
|
-
operationId: finalOperationId,
|
|
174
|
-
summary: operation.summary || '',
|
|
175
|
-
deprecated: Boolean(operation.deprecated),
|
|
176
|
-
method: normalizedMethod,
|
|
177
|
-
path: route,
|
|
178
|
-
security,
|
|
179
|
-
requestBodyRequired: requestBody.required,
|
|
180
|
-
requestContentTypes: requestBody.contentTypes,
|
|
181
|
-
parameterNames: {
|
|
182
|
-
path: parameters.path,
|
|
183
|
-
query: parameters.query,
|
|
184
|
-
header: parameters.header
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
operations.sort((a, b) => {
|
|
191
|
-
if (a.path !== b.path) {
|
|
192
|
-
return a.path.localeCompare(b.path);
|
|
193
|
-
}
|
|
194
|
-
return a.method.localeCompare(b.method);
|
|
195
|
-
});
|
|
196
|
-
|
|
197
|
-
return operations;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
async function readSpecFile(relativePath) {
|
|
201
|
-
const absolutePath = path.join(ROOT, relativePath);
|
|
202
|
-
const raw = await readFile(absolutePath, 'utf8');
|
|
203
|
-
return JSON.parse(raw);
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
async function readOptionalSpecFile(relativePath) {
|
|
207
|
-
try {
|
|
208
|
-
return await readSpecFile(relativePath);
|
|
209
|
-
} catch (error) {
|
|
210
|
-
if (error?.code === 'ENOENT') {
|
|
211
|
-
return null;
|
|
212
|
-
}
|
|
213
|
-
throw error;
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
function buildServices(specEntries) {
|
|
218
|
-
const services = {};
|
|
219
|
-
|
|
220
|
-
for (const specEntry of specEntries) {
|
|
221
|
-
const { service, file, doc } = specEntry;
|
|
222
|
-
const firstServer = Array.isArray(doc.servers) ? doc.servers[0] : undefined;
|
|
223
|
-
|
|
224
|
-
services[service] = {
|
|
225
|
-
key: service,
|
|
226
|
-
source: file,
|
|
227
|
-
title: doc.info?.title || '',
|
|
228
|
-
version: doc.info?.version || '',
|
|
229
|
-
server: normalizeServer(firstServer),
|
|
230
|
-
globalSecurity: Array.isArray(doc.security) ? doc.security : [],
|
|
231
|
-
operations: collectOperations(doc)
|
|
232
|
-
};
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
return services;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
14
|
function renderModule(generated) {
|
|
239
15
|
const payload = JSON.stringify(generated, null, 2);
|
|
240
16
|
|
|
@@ -249,53 +25,6 @@ function renderModule(generated) {
|
|
|
249
25
|
].join('\n');
|
|
250
26
|
}
|
|
251
27
|
|
|
252
|
-
// Enum values are documented inline in dbref descriptions as `N`=LABEL pairs,
|
|
253
|
-
// e.g. "Status (`0`=NOTSTARTED, `1`=AWAITINGACCEPTANCE, ...)". Extract them so
|
|
254
|
-
// the client can validate enum inputs and suggest valid values.
|
|
255
|
-
function parseEnum(description) {
|
|
256
|
-
if (typeof description !== 'string') return null;
|
|
257
|
-
const out = {};
|
|
258
|
-
let count = 0;
|
|
259
|
-
const re = /`(-?\d+)`\s*=\s*([A-Za-z0-9_]+)/g;
|
|
260
|
-
let match;
|
|
261
|
-
while ((match = re.exec(description)) !== null) {
|
|
262
|
-
out[match[1]] = match[2];
|
|
263
|
-
count += 1;
|
|
264
|
-
}
|
|
265
|
-
return count >= 2 ? out : null;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// Compact field/enum/foreign-key map derived from openapi/dbref.json. Much
|
|
269
|
-
// smaller than the raw dbref (drops storage, collation, constraints, triggers,
|
|
270
|
-
// stats, etc.) so it ships cheaply and powers runtime introspection.
|
|
271
|
-
function buildSchema(dbref) {
|
|
272
|
-
const schema = {};
|
|
273
|
-
if (!Array.isArray(dbref)) return schema;
|
|
274
|
-
|
|
275
|
-
for (const entity of dbref) {
|
|
276
|
-
if (!entity || typeof entity !== 'object' || !entity.name || !Array.isArray(entity.fields)) {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const fields = {};
|
|
281
|
-
for (const field of entity.fields) {
|
|
282
|
-
if (!field || typeof field !== 'object' || !field.name) continue;
|
|
283
|
-
const def = { type: field.type || 'unknown' };
|
|
284
|
-
if (field.indexed) def.indexed = true;
|
|
285
|
-
if (Array.isArray(field.fkeys) && field.fkeys.length > 0 && field.fkeys[0].table) {
|
|
286
|
-
def.fk = field.fkeys[0].table;
|
|
287
|
-
}
|
|
288
|
-
const enumValues = parseEnum(field.description);
|
|
289
|
-
if (enumValues) def.enum = enumValues;
|
|
290
|
-
fields[field.name] = def;
|
|
291
|
-
}
|
|
292
|
-
|
|
293
|
-
schema[entity.name] = { type: entity.type || 'table', fields };
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
return schema;
|
|
297
|
-
}
|
|
298
|
-
|
|
299
28
|
function renderSchemaModule(schema) {
|
|
300
29
|
return [
|
|
301
30
|
'// This file is auto-generated by scripts/generate-client.mjs',
|
|
@@ -311,7 +40,7 @@ async function main() {
|
|
|
311
40
|
const specEntries = [];
|
|
312
41
|
|
|
313
42
|
for (const source of SOURCES) {
|
|
314
|
-
const doc = await
|
|
43
|
+
const doc = await loadSpec(path.join(ROOT, source.file));
|
|
315
44
|
specEntries.push({ ...source, doc });
|
|
316
45
|
}
|
|
317
46
|
|
|
@@ -326,7 +55,7 @@ async function main() {
|
|
|
326
55
|
await mkdir(path.dirname(outputFile), { recursive: true });
|
|
327
56
|
await writeFile(outputFile, renderModule(generated), 'utf8');
|
|
328
57
|
|
|
329
|
-
const dbref = await
|
|
58
|
+
const dbref = await loadOptionalSpec(path.join(ROOT, 'openapi/dbref.json'));
|
|
330
59
|
const schema = buildSchema(dbref);
|
|
331
60
|
await writeFile(path.join(ROOT, 'src/generated/schema.js'), renderSchemaModule(schema), 'utf8');
|
|
332
61
|
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Producer: emit an Open Knowledge Format (OKF v0.1) bundle for the ZeyOS data
|
|
3
|
+
// model under okf/. Structural content (entity Schema/FK/Enums/Indexes/Operations)
|
|
4
|
+
// is generated from openapi/{api,dbref}.json into managed blocks; curated content
|
|
5
|
+
// (metrics, playbooks, concepts, entity notes) is seeded from scripts/data and
|
|
6
|
+
// then owned by humans/the refiner. Deterministic: re-running with unchanged
|
|
7
|
+
// specs produces no diff (log.md only grows on real schema changes).
|
|
8
|
+
|
|
9
|
+
import { readFile, writeFile, mkdir } from 'node:fs/promises';
|
|
10
|
+
import { existsSync } from 'node:fs';
|
|
11
|
+
import { createHash } from 'node:crypto';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
|
|
15
|
+
import { buildEntityModel, collectOperations, loadSpec, loadOptionalSpec } from './lib/spec-model.mjs';
|
|
16
|
+
import {
|
|
17
|
+
OKF_VERSION, renderEntityGeneratedBody, spliceConcept, renderIndex, renderRootIndex,
|
|
18
|
+
diffEntityModels, prependLogEntry, replaceManagedBlock
|
|
19
|
+
} from './lib/okf.mjs';
|
|
20
|
+
import { ENTITY_META, METRICS, PLAYBOOKS, CONCEPTS } from './data/okf-curation.mjs';
|
|
21
|
+
|
|
22
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const OKF_DIR = path.join(ROOT, 'okf');
|
|
24
|
+
// Producer-internal state for the log.md schema diff. Lives outside okf/ (so the
|
|
25
|
+
// published bundle stays pure OKF content) and outside the npm `files` allowlist
|
|
26
|
+
// (so it is not shipped), but is committed so in-repo regenerations can diff.
|
|
27
|
+
const SNAPSHOT_FILE = path.join(ROOT, 'scripts/.okf-snapshot.json');
|
|
28
|
+
|
|
29
|
+
const VERB_RE = /^(list|get|create|update|delete|exists)/;
|
|
30
|
+
const CLUSTER_ORDER = ['crm', 'work', 'messaging', 'outreach', 'knowledge', 'collaboration', 'billing', 'collections', 'commerce', 'platform', 'reference'];
|
|
31
|
+
const CLUSTER_LABEL = {
|
|
32
|
+
crm: 'CRM & Customer', work: 'Work & Delivery', messaging: 'Messaging', outreach: 'Outreach',
|
|
33
|
+
knowledge: 'Knowledge', collaboration: 'Collaboration', billing: 'Billing & Payments',
|
|
34
|
+
collections: 'Collections', commerce: 'Commerce & Inventory', platform: 'Platform & Schema', reference: 'Reference'
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function resourceFromPath(p) {
|
|
38
|
+
if (typeof p !== 'string') return null;
|
|
39
|
+
for (const segment of p.split('/')) {
|
|
40
|
+
if (segment && !segment.startsWith('{')) return segment;
|
|
41
|
+
}
|
|
42
|
+
return null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Group an entity's REST operations into list/get/create/update/delete/exists by
|
|
46
|
+
// the operationId verb prefix. operationIds come straight from api.json, so this
|
|
47
|
+
// is the canonical vocabulary (no noun→opId guessing).
|
|
48
|
+
function operationsByResource(apiDoc) {
|
|
49
|
+
const byResource = new Map();
|
|
50
|
+
for (const op of collectOperations(apiDoc)) {
|
|
51
|
+
const resource = resourceFromPath(op.path);
|
|
52
|
+
if (!resource) continue;
|
|
53
|
+
if (!byResource.has(resource)) byResource.set(resource, {});
|
|
54
|
+
const bucket = byResource.get(resource);
|
|
55
|
+
const m = VERB_RE.exec(op.operationId);
|
|
56
|
+
const key = m ? m[1] : op.operationId;
|
|
57
|
+
if (!bucket[key]) bucket[key] = op.operationId;
|
|
58
|
+
}
|
|
59
|
+
return byResource;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// "listMailingLists" → "Mailing Lists"; "getDunningNotice" → "Dunning Notice".
|
|
63
|
+
function titleFromOps(ops, fallbackNoun) {
|
|
64
|
+
const op = ops.list || ops.get || null;
|
|
65
|
+
if (!op) return fallbackNoun.charAt(0).toUpperCase() + fallbackNoun.slice(1);
|
|
66
|
+
const stripped = op.replace(VERB_RE, '');
|
|
67
|
+
return stripped.replace(/([a-z0-9])([A-Z])/g, '$1 $2').trim() || fallbackNoun;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Authoritative entity → operationId table, derived from api.json. Injected into
|
|
71
|
+
// the shared skill reference so the (previously hand-maintained, drift-prone)
|
|
72
|
+
// operationId table is generated and always correct. Links resolve from
|
|
73
|
+
// agents/shared/ back up to okf/.
|
|
74
|
+
function renderOperationIdTable(docEntities, opsByResource) {
|
|
75
|
+
const cols = ['list', 'get', 'create', 'update', 'delete', 'exists'];
|
|
76
|
+
const header = `| Entity | Concept | ${cols.join(' | ')} |\n|${'---|'.repeat(cols.length + 2)}`;
|
|
77
|
+
const rows = docEntities.map((name) => {
|
|
78
|
+
const ops = opsByResource.get(name) || {};
|
|
79
|
+
const cells = cols.map((c) => (ops[c] ? `\`${ops[c]}\`` : '—'));
|
|
80
|
+
return `| \`${name}\` | [↗](../../okf/entities/${name}.md) | ${cells.join(' | ')} |`;
|
|
81
|
+
});
|
|
82
|
+
return `${header}\n${rows.join('\n')}`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function renderCuratedDoc({ type, title, description, tags, body }) {
|
|
86
|
+
const fm = [`type: ${type}`, `title: ${title}`];
|
|
87
|
+
if (description) fm.push(`description: ${JSON.stringify(description)}`);
|
|
88
|
+
if (tags?.length) fm.push(`tags: [${tags.join(', ')}]`);
|
|
89
|
+
return `---\n${fm.join('\n')}\n---\n\n${body.replace(/\s+$/, '')}\n`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async function readIfExists(file) {
|
|
93
|
+
return existsSync(file) ? readFile(file, 'utf8') : null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
async function writeFileEnsured(file, content) {
|
|
97
|
+
await mkdir(path.dirname(file), { recursive: true });
|
|
98
|
+
await writeFile(file, content, 'utf8');
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function seedIfAbsent(file, content) {
|
|
102
|
+
if (existsSync(file)) return false;
|
|
103
|
+
await writeFileEnsured(file, content);
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function sourceSnapshot(docEntities, model, opsByResource) {
|
|
108
|
+
const payload = docEntities.map((name) => ({
|
|
109
|
+
name,
|
|
110
|
+
fields: model[name].fields.map((f) => `${f.name}:${f.type}:${f.notnull ? 1 : 0}:${f.fk ? f.fk.table : ''}:${f.enum ? Object.keys(f.enum).join('|') : ''}`),
|
|
111
|
+
ops: Object.values(opsByResource.get(name) || {}).sort()
|
|
112
|
+
}));
|
|
113
|
+
return createHash('sha256').update(JSON.stringify(payload)).digest('hex').slice(0, 12);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function main() {
|
|
117
|
+
const apiDoc = await loadSpec(path.join(ROOT, 'openapi/api.json'));
|
|
118
|
+
const dbref = await loadOptionalSpec(path.join(ROOT, 'openapi/dbref.json'));
|
|
119
|
+
const model = buildEntityModel(dbref);
|
|
120
|
+
const opsByResource = operationsByResource(apiDoc);
|
|
121
|
+
|
|
122
|
+
// API-backed entities that also have a dbref schema get an entity concept doc.
|
|
123
|
+
const docEntities = [...opsByResource.keys()].filter((name) => model[name]).sort();
|
|
124
|
+
const hasDoc = new Set(docEntities);
|
|
125
|
+
const linkForEntity = (name) => (hasDoc.has(name) ? `/entities/${name}.md` : null);
|
|
126
|
+
|
|
127
|
+
// ── Entity concept docs (managed-block splice) ──
|
|
128
|
+
for (const name of docEntities) {
|
|
129
|
+
const entity = model[name];
|
|
130
|
+
const ops = opsByResource.get(name) || {};
|
|
131
|
+
const meta = ENTITY_META[name] || {};
|
|
132
|
+
const generatedBody = renderEntityGeneratedBody({ entity, ops, linkForEntity });
|
|
133
|
+
const visibilityColumn = entity.fields.some((f) => f.name === 'visibility');
|
|
134
|
+
|
|
135
|
+
const frontmatter = {
|
|
136
|
+
type: 'ZeyOS Entity',
|
|
137
|
+
title: titleFromOps(ops, name),
|
|
138
|
+
description: meta.description || `ZeyOS \`${name}\` records.`,
|
|
139
|
+
resource: `zeyos://api/${name}`,
|
|
140
|
+
tags: [...(meta.tags || []), 'generated'],
|
|
141
|
+
api_backed: true,
|
|
142
|
+
list_operation: ops.list || undefined,
|
|
143
|
+
visibility_column: visibilityColumn
|
|
144
|
+
};
|
|
145
|
+
|
|
146
|
+
const seedBody = meta.note ? `# Notes\n\n${meta.note}` : '';
|
|
147
|
+
const file = path.join(OKF_DIR, 'entities', `${name}.md`);
|
|
148
|
+
const existing = await readIfExists(file);
|
|
149
|
+
await writeFileEnsured(file, spliceConcept({ existing, frontmatter, generatedBody, seedBody }));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// ── Curated narrative docs (seed-if-absent) ──
|
|
153
|
+
const curatedGroups = [
|
|
154
|
+
{ dir: 'metrics', type: 'Metric', docs: METRICS },
|
|
155
|
+
{ dir: 'playbooks', type: 'Playbook', docs: PLAYBOOKS },
|
|
156
|
+
{ dir: 'concepts', type: 'Reference', docs: CONCEPTS }
|
|
157
|
+
];
|
|
158
|
+
for (const group of curatedGroups) {
|
|
159
|
+
for (const doc of group.docs) {
|
|
160
|
+
const file = path.join(OKF_DIR, group.dir, `${doc.id}.md`);
|
|
161
|
+
await seedIfAbsent(file, renderCuratedDoc({ type: group.type, ...doc }));
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ── Index files (derived; always regenerated) ──
|
|
166
|
+
const byCluster = new Map();
|
|
167
|
+
for (const name of docEntities) {
|
|
168
|
+
const cluster = (ENTITY_META[name]?.tags || ['platform'])[0];
|
|
169
|
+
if (!byCluster.has(cluster)) byCluster.set(cluster, []);
|
|
170
|
+
byCluster.get(cluster).push(name);
|
|
171
|
+
}
|
|
172
|
+
const entitySections = CLUSTER_ORDER.filter((c) => byCluster.has(c)).map((cluster) => ({
|
|
173
|
+
heading: CLUSTER_LABEL[cluster] || cluster,
|
|
174
|
+
items: byCluster.get(cluster).sort().map((name) => ({
|
|
175
|
+
title: titleFromOps(opsByResource.get(name) || {}, name),
|
|
176
|
+
url: `${name}.md`,
|
|
177
|
+
description: ENTITY_META[name]?.description || ''
|
|
178
|
+
}))
|
|
179
|
+
}));
|
|
180
|
+
await writeFileEnsured(path.join(OKF_DIR, 'entities', 'index.md'), renderIndex(entitySections));
|
|
181
|
+
|
|
182
|
+
for (const group of curatedGroups) {
|
|
183
|
+
const items = [...group.docs]
|
|
184
|
+
.sort((a, b) => a.title.localeCompare(b.title))
|
|
185
|
+
.map((doc) => ({ title: doc.title, url: `${doc.id}.md`, description: doc.description }));
|
|
186
|
+
await writeFileEnsured(path.join(OKF_DIR, group.dir, 'index.md'), renderIndex([{ heading: group.dir.charAt(0).toUpperCase() + group.dir.slice(1), items }]));
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// ── Root index (only index allowed frontmatter: okf_version + source_snapshot) ──
|
|
190
|
+
const snapshot = sourceSnapshot(docEntities, model, opsByResource);
|
|
191
|
+
const rootSections = [{
|
|
192
|
+
heading: 'ZeyOS Knowledge Bundle',
|
|
193
|
+
items: [
|
|
194
|
+
{ title: 'Entities', url: 'entities/', description: `${docEntities.length} API-backed entity concepts (schema, foreign keys, enums, indexes, operations).` },
|
|
195
|
+
{ title: 'Metrics', url: 'metrics/', description: 'Business metric definitions.' },
|
|
196
|
+
{ title: 'Playbooks', url: 'playbooks/', description: 'Step-by-step query workflows.' },
|
|
197
|
+
{ title: 'Concepts', url: 'concepts/', description: 'Cross-cutting query rules and footguns.' }
|
|
198
|
+
]
|
|
199
|
+
}];
|
|
200
|
+
await writeFileEnsured(path.join(OKF_DIR, 'index.md'), renderRootIndex({ sourceSnapshot: snapshot, sections: rootSections }));
|
|
201
|
+
|
|
202
|
+
// ── Freshness: diff against the last snapshot → append log.md on change ──
|
|
203
|
+
const prevSnapshot = existsSync(SNAPSHOT_FILE) ? JSON.parse(await readFile(SNAPSHOT_FILE, 'utf8')) : null;
|
|
204
|
+
// Compact model: only the fields diffEntityModels reads (name/type/enum/fk), so the
|
|
205
|
+
// committed snapshot stays small (drops descriptions, indexes, nullability, defaults).
|
|
206
|
+
const currentModel = Object.fromEntries(docEntities.map((name) => [name, {
|
|
207
|
+
fields: model[name].fields.map((f) => ({ name: f.name, type: f.type, enum: f.enum, fk: f.fk ? { table: f.fk.table } : null }))
|
|
208
|
+
}]));
|
|
209
|
+
const logFile = path.join(OKF_DIR, 'log.md');
|
|
210
|
+
if (!prevSnapshot) {
|
|
211
|
+
const existingLog = await readIfExists(logFile);
|
|
212
|
+
if (!existingLog) {
|
|
213
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
214
|
+
await writeFileEnsured(logFile, prependLogEntry({ existing: null, date, changes: [{ kind: 'Initialization', text: `OKF bundle initialized with ${docEntities.length} entity concepts.` }] }));
|
|
215
|
+
}
|
|
216
|
+
} else {
|
|
217
|
+
const changes = diffEntityModels(prevSnapshot.model, currentModel);
|
|
218
|
+
if (changes.length) {
|
|
219
|
+
const date = new Date().toISOString().slice(0, 10);
|
|
220
|
+
const existingLog = await readIfExists(logFile);
|
|
221
|
+
await writeFileEnsured(logFile, prependLogEntry({ existing: existingLog, date, changes }));
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
await writeFileEnsured(SNAPSHOT_FILE, `${JSON.stringify({ snapshot, model: currentModel }, null, 2)}\n`);
|
|
225
|
+
|
|
226
|
+
// ── Derive the shared skill reference's operationId table from OKF (canonical) ──
|
|
227
|
+
const refFile = path.join(ROOT, 'agents/shared/zeyos-entity-reference.md');
|
|
228
|
+
const refExisting = await readIfExists(refFile);
|
|
229
|
+
if (refExisting) {
|
|
230
|
+
const filled = replaceManagedBlock(refExisting, renderOperationIdTable(docEntities, opsByResource));
|
|
231
|
+
if (filled) await writeFile(refFile, filled, 'utf8');
|
|
232
|
+
else process.stderr.write(`[okf] note: ${path.relative(ROOT, refFile)} has no okf:generated markers; operationId table not injected.\n`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
process.stdout.write(`Generated OKF bundle (v${OKF_VERSION}) → okf/ (${docEntities.length} entities, snapshot ${snapshot})\n`);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
main().catch((error) => {
|
|
239
|
+
console.error(error);
|
|
240
|
+
process.exit(1);
|
|
241
|
+
});
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export function resolveLiveConfig({ known, liveConfig = {}, env = process.env }) {
|
|
2
|
+
let url = known.url ?? env.npm_config_url ?? null;
|
|
3
|
+
let instance = known.instance ?? env.npm_config_instance ?? null;
|
|
4
|
+
|
|
5
|
+
if (known.live && !url && !instance) {
|
|
6
|
+
if (liveConfig.url) {
|
|
7
|
+
url = liveConfig.url;
|
|
8
|
+
} else {
|
|
9
|
+
instance = liveConfig.instance ?? null;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
return {
|
|
14
|
+
url,
|
|
15
|
+
instance,
|
|
16
|
+
clientIdArg: known.clientId ?? env.npm_config_client_id ?? env.ZEYOS_CLIENT_ID ?? liveConfig.clientId ?? null,
|
|
17
|
+
clientSecretArg: known.clientSecret ?? env.npm_config_client_secret ?? env.ZEYOS_CLIENT_SECRET ?? liveConfig.clientSecret ?? null,
|
|
18
|
+
port: known.port ?? env.npm_config_port ?? ((url || instance) ? liveConfig.port ?? null : null)
|
|
19
|
+
};
|
|
20
|
+
}
|