@odla-ai/cli 0.27.4 → 0.27.5
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 +9 -9
- package/dist/bin.cjs +95 -58
- package/dist/bin.cjs.map +1 -1
- package/dist/bin.js +1 -1
- package/dist/{chunk-GENTB3VO.js → chunk-DQOJ4S6H.js} +96 -59
- package/dist/chunk-DQOJ4S6H.js.map +1 -0
- package/dist/index.cjs +95 -58
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +7 -5
- package/dist/index.d.ts +7 -5
- package/dist/index.js +1 -1
- package/package.json +1 -1
- package/skills/odla/references/sdks.md +5 -4
- package/dist/chunk-GENTB3VO.js.map +0 -1
package/dist/index.cjs
CHANGED
|
@@ -1069,6 +1069,32 @@ var import_node_path4 = require("path");
|
|
|
1069
1069
|
var import_node_url = require("url");
|
|
1070
1070
|
var import_apps = require("@odla-ai/apps");
|
|
1071
1071
|
|
|
1072
|
+
// src/ai-config-validation.ts
|
|
1073
|
+
function validateAiConfig(cfg, path) {
|
|
1074
|
+
if (cfg.ai === void 0) return;
|
|
1075
|
+
if (!isRecord4(cfg.ai)) throw new Error(`${path}: ai must be an object`);
|
|
1076
|
+
assertOnly(cfg.ai, ["mode", "provider", "model", "keyEnv", "secretName"], `${path}: ai`);
|
|
1077
|
+
if (cfg.ai.mode !== void 0 && cfg.ai.mode !== "hosted" && cfg.ai.mode !== "byok") {
|
|
1078
|
+
throw new Error(`${path}: ai.mode must be hosted or byok`);
|
|
1079
|
+
}
|
|
1080
|
+
if (cfg.ai.mode === "byok" && !safeText(cfg.ai.provider, 100)) {
|
|
1081
|
+
throw new Error(`${path}: ai.provider is required when ai.mode is byok`);
|
|
1082
|
+
}
|
|
1083
|
+
if (cfg.ai.mode === "hosted" && (cfg.ai.provider || cfg.ai.keyEnv || cfg.ai.secretName)) {
|
|
1084
|
+
throw new Error(`${path}: hosted ai cannot configure a provider, keyEnv, or secretName`);
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
function assertOnly(value2, allowed, label) {
|
|
1088
|
+
const extra = Object.keys(value2).find((key) => !allowed.includes(key));
|
|
1089
|
+
if (extra) throw new Error(`${label}.${extra} is not supported`);
|
|
1090
|
+
}
|
|
1091
|
+
function isRecord4(value2) {
|
|
1092
|
+
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
1093
|
+
}
|
|
1094
|
+
function safeText(value2, max) {
|
|
1095
|
+
return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
|
|
1096
|
+
}
|
|
1097
|
+
|
|
1072
1098
|
// src/integration-validation.ts
|
|
1073
1099
|
function validateIntegrations(cfg, path, defaultServices) {
|
|
1074
1100
|
if (cfg.integrations === void 0) return;
|
|
@@ -1076,16 +1102,16 @@ function validateIntegrations(cfg, path, defaultServices) {
|
|
|
1076
1102
|
const ids = /* @__PURE__ */ new Set();
|
|
1077
1103
|
for (const [index, integration] of cfg.integrations.entries()) {
|
|
1078
1104
|
const at = `${path}: integrations[${index}]`;
|
|
1079
|
-
if (!
|
|
1105
|
+
if (!isRecord5(integration)) throw new Error(`${at} must be an object`);
|
|
1080
1106
|
if (!validId(integration.id)) throw new Error(`${at}.id must be lowercase letters, numbers, and hyphens`);
|
|
1081
1107
|
if (ids.has(integration.id)) throw new Error(`${path}: duplicate integration id "${integration.id}"`);
|
|
1082
1108
|
ids.add(integration.id);
|
|
1083
|
-
if (!
|
|
1084
|
-
if (!
|
|
1085
|
-
if (integration.schema !== void 0 && (!
|
|
1109
|
+
if (!safeText2(integration.title, 200)) throw new Error(`${at}.title is required`);
|
|
1110
|
+
if (!safeText2(integration.npm, 200)) throw new Error(`${at}.npm is required`);
|
|
1111
|
+
if (integration.schema !== void 0 && (!isRecord5(integration.schema) || !isRecord5(integration.schema.entities))) {
|
|
1086
1112
|
throw new Error(`${at}.schema must contain an entities object`);
|
|
1087
1113
|
}
|
|
1088
|
-
if (integration.rules !== void 0 && !
|
|
1114
|
+
if (integration.rules !== void 0 && !isRecord5(integration.rules)) throw new Error(`${at}.rules must be an object`);
|
|
1089
1115
|
validateSeeds(integration, at);
|
|
1090
1116
|
validateProbes(integration, at);
|
|
1091
1117
|
}
|
|
@@ -1099,13 +1125,13 @@ function validateSeeds(integration, at) {
|
|
|
1099
1125
|
const ids = /* @__PURE__ */ new Set();
|
|
1100
1126
|
for (const [index, seed] of integration.seeds.entries()) {
|
|
1101
1127
|
const sat = `${at}.seeds[${index}]`;
|
|
1102
|
-
if (!
|
|
1128
|
+
if (!isRecord5(seed) || !safeText2(seed.id, 200) || !safeText2(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
|
|
1103
1129
|
if (ids.has(seed.id)) throw new Error(`${at} has duplicate seed id "${seed.id}"`);
|
|
1104
1130
|
ids.add(seed.id);
|
|
1105
|
-
if (!
|
|
1131
|
+
if (!isRecord5(seed.key) || !safeText2(seed.key.attr, 200) || !safeText2(seed.key.value, 2048)) {
|
|
1106
1132
|
throw new Error(`${sat}.key requires string attr and value`);
|
|
1107
1133
|
}
|
|
1108
|
-
if (!
|
|
1134
|
+
if (!isRecord5(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
|
|
1109
1135
|
if (Object.hasOwn(seed.attrs, seed.key.attr) && seed.attrs[seed.key.attr] !== seed.key.value) {
|
|
1110
1136
|
throw new Error(`${sat}.attrs.${seed.key.attr} conflicts with its natural key`);
|
|
1111
1137
|
}
|
|
@@ -1116,16 +1142,16 @@ function validateProbes(integration, at) {
|
|
|
1116
1142
|
if (!Array.isArray(integration.probes)) throw new Error(`${at}.probes must be an array`);
|
|
1117
1143
|
for (const [index, probe] of integration.probes.entries()) {
|
|
1118
1144
|
const pat = `${at}.probes[${index}]`;
|
|
1119
|
-
if (!
|
|
1145
|
+
if (!isRecord5(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
|
|
1120
1146
|
if (!Number.isInteger(probe.expectedStatus) || probe.expectedStatus < 100 || probe.expectedStatus > 599) {
|
|
1121
1147
|
throw new Error(`${pat}.expectedStatus must be an HTTP status`);
|
|
1122
1148
|
}
|
|
1123
1149
|
}
|
|
1124
1150
|
}
|
|
1125
|
-
function
|
|
1151
|
+
function isRecord5(value2) {
|
|
1126
1152
|
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
1127
1153
|
}
|
|
1128
|
-
function
|
|
1154
|
+
function safeText2(value2, max) {
|
|
1129
1155
|
return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
|
|
1130
1156
|
}
|
|
1131
1157
|
function safeProbePath(value2) {
|
|
@@ -1255,6 +1281,7 @@ function validateRawConfig(raw, path) {
|
|
|
1255
1281
|
if (cfg.services !== void 0 && (!Array.isArray(cfg.services) || cfg.services.some((service) => typeof service !== "string" || !service.trim()))) {
|
|
1256
1282
|
throw new Error(`${path}: services must be an array of non-empty names`);
|
|
1257
1283
|
}
|
|
1284
|
+
validateAiConfig(cfg, path);
|
|
1258
1285
|
validateIntegrations(cfg, path, DEFAULT_SERVICES);
|
|
1259
1286
|
}
|
|
1260
1287
|
function validateCalendarConfig(cfg, envs, services, path) {
|
|
@@ -1263,11 +1290,11 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1263
1290
|
if (enabled) throw new Error(`${path}: calendar.google is required when services includes "calendar"`);
|
|
1264
1291
|
return;
|
|
1265
1292
|
}
|
|
1266
|
-
if (!
|
|
1267
|
-
|
|
1268
|
-
if (!
|
|
1293
|
+
if (!isRecord6(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
|
|
1294
|
+
assertOnly2(cfg.calendar, ["google"], `${path}: calendar`);
|
|
1295
|
+
if (!isRecord6(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
|
|
1269
1296
|
const google = cfg.calendar.google;
|
|
1270
|
-
|
|
1297
|
+
assertOnly2(
|
|
1271
1298
|
google,
|
|
1272
1299
|
["availabilityCalendars", "calendars", "bookingCalendar", "bookingPageUrl"],
|
|
1273
1300
|
`${path}: calendar.google`
|
|
@@ -1277,7 +1304,7 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1277
1304
|
throw new Error(`${path}: calendar.google requires exactly one of availabilityCalendars or calendars (legacy)`);
|
|
1278
1305
|
}
|
|
1279
1306
|
const availability = google[availabilityKey];
|
|
1280
|
-
if (!
|
|
1307
|
+
if (!isRecord6(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
|
|
1281
1308
|
const unknownEnv = Object.keys(availability).find((env) => !envs.includes(env));
|
|
1282
1309
|
if (unknownEnv) throw new Error(`${path}: calendar.google.${availabilityKey}.${unknownEnv} is not in config envs`);
|
|
1283
1310
|
for (const env of envs) {
|
|
@@ -1288,22 +1315,22 @@ function validateCalendarConfig(cfg, envs, services, path) {
|
|
|
1288
1315
|
if (ids.length > 10) {
|
|
1289
1316
|
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} must contain at most 10 calendar ids`);
|
|
1290
1317
|
}
|
|
1291
|
-
if (ids.some((id) => !
|
|
1318
|
+
if (ids.some((id) => !safeText3(id, 1024))) {
|
|
1292
1319
|
throw new Error(`${path}: calendar.google.${availabilityKey}.${env} contains an invalid calendar id`);
|
|
1293
1320
|
}
|
|
1294
1321
|
}
|
|
1295
1322
|
if (google.bookingCalendar !== void 0) {
|
|
1296
|
-
if (!
|
|
1323
|
+
if (!isRecord6(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
|
|
1297
1324
|
const unknownBookingEnv = Object.keys(google.bookingCalendar).find((env) => !envs.includes(env));
|
|
1298
1325
|
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingCalendar.${unknownBookingEnv} is not in config envs`);
|
|
1299
1326
|
for (const [env, value2] of Object.entries(google.bookingCalendar)) {
|
|
1300
|
-
if (!
|
|
1327
|
+
if (!safeText3(value2, 1024)) {
|
|
1301
1328
|
throw new Error(`${path}: calendar.google.bookingCalendar.${env} must be a calendar id`);
|
|
1302
1329
|
}
|
|
1303
1330
|
}
|
|
1304
1331
|
}
|
|
1305
1332
|
if (google.bookingPageUrl !== void 0) {
|
|
1306
|
-
if (!
|
|
1333
|
+
if (!isRecord6(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
|
|
1307
1334
|
const unknownBookingEnv = Object.keys(google.bookingPageUrl).find((env) => !envs.includes(env));
|
|
1308
1335
|
if (unknownBookingEnv) throw new Error(`${path}: calendar.google.bookingPageUrl.${unknownBookingEnv} is not in config envs`);
|
|
1309
1336
|
for (const [env, value2] of Object.entries(google.bookingPageUrl)) {
|
|
@@ -1326,14 +1353,14 @@ function validateServices(services, path) {
|
|
|
1326
1353
|
}
|
|
1327
1354
|
}
|
|
1328
1355
|
}
|
|
1329
|
-
function
|
|
1356
|
+
function assertOnly2(value2, allowed, label) {
|
|
1330
1357
|
const extra = Object.keys(value2).find((key) => !allowed.includes(key));
|
|
1331
1358
|
if (extra) throw new Error(`${label}.${extra} is not supported`);
|
|
1332
1359
|
}
|
|
1333
|
-
function
|
|
1360
|
+
function isRecord6(value2) {
|
|
1334
1361
|
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
1335
1362
|
}
|
|
1336
|
-
function
|
|
1363
|
+
function safeText3(value2, max) {
|
|
1337
1364
|
return typeof value2 === "string" && value2.trim().length > 0 && value2.length <= max && !/[\u0000-\u001f\u007f]/.test(value2);
|
|
1338
1365
|
}
|
|
1339
1366
|
function safeHttpsUrl(value2) {
|
|
@@ -3130,7 +3157,7 @@ async function assertTenantAdminAccess(doFetch, cfg, env, token) {
|
|
|
3130
3157
|
`${env}: you are not an owner of "${cfg.app.id}" (tenant ${tenantId}) \u2014 nothing was minted or written; ask an existing owner to run "odla-ai app owners add <your-email>", then re-run provision`
|
|
3131
3158
|
);
|
|
3132
3159
|
}
|
|
3133
|
-
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await
|
|
3160
|
+
throw new Error(`${env}: tenant access preflight (${tenantId}) failed: ${res.status} ${await safeText4(res)}`);
|
|
3134
3161
|
}
|
|
3135
3162
|
async function postJson(doFetch, url, bearer, body) {
|
|
3136
3163
|
const res = await doFetch(url, {
|
|
@@ -3138,7 +3165,7 @@ async function postJson(doFetch, url, bearer, body) {
|
|
|
3138
3165
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
3139
3166
|
body: JSON.stringify(body)
|
|
3140
3167
|
});
|
|
3141
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await
|
|
3168
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await safeText4(res)}`);
|
|
3142
3169
|
}
|
|
3143
3170
|
function normalizeClerkConfig(value2) {
|
|
3144
3171
|
if (!value2) return null;
|
|
@@ -3151,7 +3178,7 @@ function normalizeClerkConfig(value2) {
|
|
|
3151
3178
|
const publishableKey = envValue(cfg.publishableKey);
|
|
3152
3179
|
return publishableKey ? { publishableKey, ...cfg.audience ? { audience: cfg.audience } : {}, ...cfg.mode ? { mode: cfg.mode } : {} } : null;
|
|
3153
3180
|
}
|
|
3154
|
-
async function
|
|
3181
|
+
async function safeText4(res) {
|
|
3155
3182
|
try {
|
|
3156
3183
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
3157
3184
|
} catch {
|
|
@@ -4133,18 +4160,18 @@ function assertSeedContracts(integrations, schema) {
|
|
|
4133
4160
|
}
|
|
4134
4161
|
}
|
|
4135
4162
|
function isUniqueAttr(schema, ns, attr) {
|
|
4136
|
-
if (!
|
|
4163
|
+
if (!isRecord7(schema) || !isRecord7(schema.entities)) return false;
|
|
4137
4164
|
const entity = schema.entities[ns];
|
|
4138
|
-
if (!
|
|
4165
|
+
if (!isRecord7(entity) || !isRecord7(entity.attrs)) return false;
|
|
4139
4166
|
const definition = entity.attrs[attr];
|
|
4140
|
-
return
|
|
4167
|
+
return isRecord7(definition) && definition.unique === true;
|
|
4141
4168
|
}
|
|
4142
4169
|
function normalizeSchema(value2) {
|
|
4143
4170
|
if (value2 === void 0 || value2 === null) return { entities: {}, links: {} };
|
|
4144
|
-
if (!
|
|
4171
|
+
if (!isRecord7(value2) || !isRecord7(value2.entities)) {
|
|
4145
4172
|
throw new Error("db schema must be a serialized schema object with an entities map");
|
|
4146
4173
|
}
|
|
4147
|
-
if (value2.links !== void 0 && !
|
|
4174
|
+
if (value2.links !== void 0 && !isRecord7(value2.links)) throw new Error("db schema links must be an object");
|
|
4148
4175
|
return {
|
|
4149
4176
|
entities: { ...value2.entities },
|
|
4150
4177
|
links: { ...value2.links ?? {} }
|
|
@@ -4158,7 +4185,7 @@ function mergeMap(target, fragment, label) {
|
|
|
4158
4185
|
target[name] = value2;
|
|
4159
4186
|
}
|
|
4160
4187
|
}
|
|
4161
|
-
function
|
|
4188
|
+
function isRecord7(value2) {
|
|
4162
4189
|
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
4163
4190
|
}
|
|
4164
4191
|
|
|
@@ -4177,7 +4204,7 @@ async function doctor(options) {
|
|
|
4177
4204
|
out.log(`integrations: ${plan.integrations.length ? plan.integrations.join(", ") : "none"}`);
|
|
4178
4205
|
out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
|
|
4179
4206
|
out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
|
|
4180
|
-
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider
|
|
4207
|
+
out.log(`ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ? `byok/${cfg.ai.provider}` : "hosted" : "not enabled"}`);
|
|
4181
4208
|
if (cfg.services.includes("calendar")) {
|
|
4182
4209
|
const calendar = cfg.envs.map((env) => {
|
|
4183
4210
|
const resolved = calendarServiceConfig(cfg, env);
|
|
@@ -4198,7 +4225,9 @@ async function doctor(options) {
|
|
|
4198
4225
|
}
|
|
4199
4226
|
}
|
|
4200
4227
|
warnings.push(...integrationWarnings(database.integrations, schema, rules));
|
|
4201
|
-
if (cfg.services.includes("ai") &&
|
|
4228
|
+
if (cfg.services.includes("ai") && cfg.ai?.mode === "byok" && !cfg.ai.provider) {
|
|
4229
|
+
warnings.push("ai.mode is byok but ai.provider is not set");
|
|
4230
|
+
}
|
|
4202
4231
|
if (cfg.auth?.clerk) {
|
|
4203
4232
|
for (const [env, value2] of Object.entries(cfg.auth.clerk)) {
|
|
4204
4233
|
if (typeof value2 === "string" && value2.startsWith("$") && !process.env[value2.slice(1)]) {
|
|
@@ -4278,7 +4307,7 @@ function initProject(options) {
|
|
|
4278
4307
|
if (!services.includes(dependency)) throw new Error(`--services ${service} requires ${dependency}`);
|
|
4279
4308
|
}
|
|
4280
4309
|
}
|
|
4281
|
-
const aiProvider = options.aiProvider
|
|
4310
|
+
const aiProvider = options.aiProvider;
|
|
4282
4311
|
(0, import_node_fs13.mkdirSync)((0, import_node_path12.dirname)(configPath), { recursive: true });
|
|
4283
4312
|
(0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, "src/odla"), { recursive: true });
|
|
4284
4313
|
(0, import_node_fs13.mkdirSync)((0, import_node_path12.resolve)(rootDir, ".odla"), { recursive: true });
|
|
@@ -4305,6 +4334,15 @@ function configTemplate(input) {
|
|
|
4305
4334
|
},
|
|
4306
4335
|
},
|
|
4307
4336
|
` : "";
|
|
4337
|
+
const ai = input.aiProvider ? ` ai: {
|
|
4338
|
+
mode: "byok",
|
|
4339
|
+
provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
|
|
4340
|
+
// Optional: set this env var while running provision to store the provider
|
|
4341
|
+
// key in the app vault for each tenant.
|
|
4342
|
+
keyEnv: "${defaultKeyEnv(input.aiProvider)}",
|
|
4343
|
+
},` : ` // Hosted AI uses admin-approved models and central cost tracking.
|
|
4344
|
+
// Pass --ai-provider during init only when this app must use BYOK.
|
|
4345
|
+
ai: { mode: "hosted" },`;
|
|
4308
4346
|
return `export default {
|
|
4309
4347
|
platformUrl: process.env.ODLA_PLATFORM_URL ?? "https://odla.ai",
|
|
4310
4348
|
dbEndpoint: process.env.ODLA_ENDPOINT ?? process.env.ODLA_DB_ENDPOINT ?? "https://db.odla.ai",
|
|
@@ -4323,12 +4361,7 @@ function configTemplate(input) {
|
|
|
4323
4361
|
// When rules is omitted, the CLI generates deny-all rules from schema.
|
|
4324
4362
|
defaultRules: "deny",
|
|
4325
4363
|
},
|
|
4326
|
-
|
|
4327
|
-
provider: process.env.ODLA_AI_PROVIDER ?? "${input.aiProvider}",
|
|
4328
|
-
// Optional: set this env var while running provision to store the provider
|
|
4329
|
-
// key in the platform vault for each tenant.
|
|
4330
|
-
keyEnv: "${defaultKeyEnv(input.aiProvider)}",
|
|
4331
|
-
},
|
|
4364
|
+
${ai}
|
|
4332
4365
|
${calendar}
|
|
4333
4366
|
auth: {
|
|
4334
4367
|
clerk: {
|
|
@@ -4824,12 +4857,16 @@ async function smoke(options) {
|
|
|
4824
4857
|
out.log(` tenant: ${entry.tenantId}`);
|
|
4825
4858
|
const publicConfig = await getJson(doFetch, publicConfigUrl(cfg.platformUrl, cfg.app.id, env), void 0);
|
|
4826
4859
|
out.log(` public-config: ok`);
|
|
4827
|
-
if (cfg.ai?.provider) {
|
|
4860
|
+
if (cfg.services.includes("ai") && cfg.ai?.provider) {
|
|
4828
4861
|
const provider = publicConfig.ai?.provider ?? null;
|
|
4829
4862
|
if (provider !== cfg.ai.provider) {
|
|
4830
4863
|
throw new Error(`ai provider mismatch: expected "${cfg.ai.provider}", public-config has "${provider ?? "none"}"`);
|
|
4831
4864
|
}
|
|
4832
|
-
out.log(` ai:
|
|
4865
|
+
out.log(` ai: byok/${provider}`);
|
|
4866
|
+
} else if (cfg.services.includes("ai")) {
|
|
4867
|
+
const mode = publicConfig.ai?.mode;
|
|
4868
|
+
if (mode !== "hosted") throw new Error(`ai mode mismatch: expected "hosted", public-config has "${String(mode ?? "none")}"`);
|
|
4869
|
+
out.log(" ai: hosted");
|
|
4833
4870
|
}
|
|
4834
4871
|
if (hasO11y) out.log(` o11y: credentials present`);
|
|
4835
4872
|
if (cfg.services.includes("calendar")) {
|
|
@@ -4907,7 +4944,7 @@ async function getJson(doFetch, url, bearer) {
|
|
|
4907
4944
|
const res = await doFetch(url, {
|
|
4908
4945
|
headers: bearer ? { authorization: `Bearer ${bearer}` } : void 0
|
|
4909
4946
|
});
|
|
4910
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4947
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
4911
4948
|
return res.json();
|
|
4912
4949
|
}
|
|
4913
4950
|
async function postJson2(doFetch, url, bearer, body) {
|
|
@@ -4916,7 +4953,7 @@ async function postJson2(doFetch, url, bearer, body) {
|
|
|
4916
4953
|
headers: { authorization: `Bearer ${bearer}`, "content-type": "application/json" },
|
|
4917
4954
|
body: JSON.stringify(body)
|
|
4918
4955
|
});
|
|
4919
|
-
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await
|
|
4956
|
+
if (!res.ok) throw new Error(`${new URL(url).pathname} returned ${res.status}: ${await safeText5(res)}`);
|
|
4920
4957
|
return res.json();
|
|
4921
4958
|
}
|
|
4922
4959
|
function publicConfigUrl(platformUrl, appId, env) {
|
|
@@ -4924,7 +4961,7 @@ function publicConfigUrl(platformUrl, appId, env) {
|
|
|
4924
4961
|
url.searchParams.set("env", env);
|
|
4925
4962
|
return url.toString();
|
|
4926
4963
|
}
|
|
4927
|
-
async function
|
|
4964
|
+
async function safeText5(res) {
|
|
4928
4965
|
try {
|
|
4929
4966
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
4930
4967
|
} catch {
|
|
@@ -8754,7 +8791,7 @@ Start here:
|
|
|
8754
8791
|
|
|
8755
8792
|
Usage:
|
|
8756
8793
|
odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
|
|
8757
|
-
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
|
|
8794
|
+
odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod] [--ai-provider <byok-provider>]
|
|
8758
8795
|
odla-ai doctor [--config odla.config.mjs]
|
|
8759
8796
|
odla-ai config <diff|plan> [--config odla.config.mjs] [--email <odla-account>] [--json]
|
|
8760
8797
|
odla-ai config apply --plan <plan.json> [--idempotency-key <key>] [--email <odla-account>] [--json]
|
|
@@ -10747,7 +10784,7 @@ async function provisionIntegrationSeeds(doFetch, endpoint, tenantId, dbKey, int
|
|
|
10747
10784
|
const payload = await postJson3(doFetch, `${base}/query`, dbKey, {
|
|
10748
10785
|
query: { [seed.ns]: { $: { where: { [seed.key.attr]: seed.key.value }, limit: 1 } } }
|
|
10749
10786
|
});
|
|
10750
|
-
const rows =
|
|
10787
|
+
const rows = isRecord8(payload) && isRecord8(payload.result) ? payload.result[seed.ns] : void 0;
|
|
10751
10788
|
if (!Array.isArray(rows)) {
|
|
10752
10789
|
throw new Error(`${env}: integration ${integration.id} seed ${seed.id} query returned an invalid response`);
|
|
10753
10790
|
}
|
|
@@ -10779,7 +10816,7 @@ async function postJson3(doFetch, url, bearer, body) {
|
|
|
10779
10816
|
if (!res.ok) throw new Error(`${new URL(url).pathname} failed: ${res.status} ${await responseText(res)}`);
|
|
10780
10817
|
return res.json().catch(() => ({}));
|
|
10781
10818
|
}
|
|
10782
|
-
function
|
|
10819
|
+
function isRecord8(value2) {
|
|
10783
10820
|
return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
|
|
10784
10821
|
}
|
|
10785
10822
|
async function responseText(res) {
|
|
@@ -10849,14 +10886,14 @@ async function mintDbKey(opts, tenantId) {
|
|
|
10849
10886
|
appId: tenantId
|
|
10850
10887
|
})
|
|
10851
10888
|
});
|
|
10852
|
-
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await
|
|
10889
|
+
if (!created.ok) throw new Error(`db app create (${tenantId}) failed: ${created.status} ${await safeText6(created)}`);
|
|
10853
10890
|
res = await opts.fetch(`${opts.cfg.dbEndpoint}/admin/apps/${encodeURIComponent(tenantId)}/keys`, {
|
|
10854
10891
|
method: "POST",
|
|
10855
10892
|
headers,
|
|
10856
10893
|
body: "{}"
|
|
10857
10894
|
});
|
|
10858
10895
|
}
|
|
10859
|
-
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await
|
|
10896
|
+
if (!res.ok) throw new Error(`db key mint (${tenantId}) failed: ${res.status} ${await safeText6(res)}`);
|
|
10860
10897
|
const body = await res.json();
|
|
10861
10898
|
if (!body.key) throw new Error(`db key mint (${tenantId}) returned no key`);
|
|
10862
10899
|
return body.key;
|
|
@@ -10872,12 +10909,12 @@ async function issueO11yToken(opts) {
|
|
|
10872
10909
|
`o11y token already exists for env "${opts.env}", but its shown-once value is not in the local credentials file; run "odla-ai provision --rotate-o11y-token --push-secrets" to replace it explicitly`
|
|
10873
10910
|
);
|
|
10874
10911
|
}
|
|
10875
|
-
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await
|
|
10912
|
+
if (!res.ok) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) failed: ${res.status} ${await safeText6(res)}`);
|
|
10876
10913
|
const body = await res.json();
|
|
10877
10914
|
if (!body.token) throw new Error(`o11y token ${opts.rotateO11y ? "rotation" : "issue"} (${opts.env}) returned no token`);
|
|
10878
10915
|
return body.token;
|
|
10879
10916
|
}
|
|
10880
|
-
async function
|
|
10917
|
+
async function safeText6(res) {
|
|
10881
10918
|
try {
|
|
10882
10919
|
return redactSecrets((await res.text()).slice(0, 500));
|
|
10883
10920
|
} catch {
|
|
@@ -10920,7 +10957,7 @@ async function provision(options) {
|
|
|
10920
10957
|
const namespaces = Object.keys(integration.schema?.entities ?? {}).length;
|
|
10921
10958
|
out.log(` integration.${integration.id}: ${namespaces} namespaces, ${integration.seeds?.length ?? 0} seeds, ${integration.probes?.length ?? 0} smoke probes`);
|
|
10922
10959
|
}
|
|
10923
|
-
out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "
|
|
10960
|
+
out.log(` ai: ${cfg.services.includes("ai") ? cfg.ai?.provider ?? "hosted" : "not enabled"}`);
|
|
10924
10961
|
if (cfg.services.includes("calendar")) {
|
|
10925
10962
|
for (const env of cfg.envs) {
|
|
10926
10963
|
const calendar = calendarServiceConfig(cfg, env);
|
|
@@ -10971,11 +11008,11 @@ async function provision(options) {
|
|
|
10971
11008
|
for (const service of serviceOrder) {
|
|
10972
11009
|
if (service === "ai") {
|
|
10973
11010
|
if (cfg.ai?.provider) {
|
|
10974
|
-
await apps.setAi(cfg.app.id, env, { provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
|
|
10975
|
-
out.log(`${env}: ai configured (${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
|
|
11011
|
+
await apps.setAi(cfg.app.id, env, { mode: "byok", provider: cfg.ai.provider, ...cfg.ai.model ? { model: cfg.ai.model } : {} });
|
|
11012
|
+
out.log(`${env}: ai configured (byok ${cfg.ai.provider}${cfg.ai.model ? `/${cfg.ai.model}` : ""})`);
|
|
10976
11013
|
} else {
|
|
10977
|
-
await apps.
|
|
10978
|
-
out.log(`${env}: ai
|
|
11014
|
+
await apps.setAi(cfg.app.id, env, { mode: "hosted", ...cfg.ai?.model ? { model: cfg.ai.model } : {} });
|
|
11015
|
+
out.log(`${env}: ai configured (hosted${cfg.ai?.model ? `/${cfg.ai.model}` : ""})`);
|
|
10979
11016
|
}
|
|
10980
11017
|
} else {
|
|
10981
11018
|
const config = service === "calendar" ? calendarServiceConfig(cfg, env) : void 0;
|