@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/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 (!isRecord4(integration)) throw new Error(`${at} must be an object`);
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 (!safeText(integration.title, 200)) throw new Error(`${at}.title is required`);
1084
- if (!safeText(integration.npm, 200)) throw new Error(`${at}.npm is required`);
1085
- if (integration.schema !== void 0 && (!isRecord4(integration.schema) || !isRecord4(integration.schema.entities))) {
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 && !isRecord4(integration.rules)) throw new Error(`${at}.rules must be an object`);
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 (!isRecord4(seed) || !safeText(seed.id, 200) || !safeText(seed.ns, 200)) throw new Error(`${sat} requires id and ns`);
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 (!isRecord4(seed.key) || !safeText(seed.key.attr, 200) || !safeText(seed.key.value, 2048)) {
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 (!isRecord4(seed.attrs)) throw new Error(`${sat}.attrs must be an object`);
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 (!isRecord4(probe) || !safeProbePath(probe.path)) throw new Error(`${pat}.path must be an absolute path without query or fragment`);
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 isRecord4(value2) {
1151
+ function isRecord5(value2) {
1126
1152
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1127
1153
  }
1128
- function safeText(value2, max) {
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 (!isRecord5(cfg.calendar)) throw new Error(`${path}: calendar must be an object`);
1267
- assertOnly(cfg.calendar, ["google"], `${path}: calendar`);
1268
- if (!isRecord5(cfg.calendar.google)) throw new Error(`${path}: calendar.google must be an object`);
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
- assertOnly(
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 (!isRecord5(availability)) throw new Error(`${path}: calendar.google.${availabilityKey} must map env names to calendar ids`);
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) => !safeText2(id, 1024))) {
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 (!isRecord5(google.bookingCalendar)) throw new Error(`${path}: calendar.google.bookingCalendar must map env names to one calendar id`);
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 (!safeText2(value2, 1024)) {
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 (!isRecord5(google.bookingPageUrl)) throw new Error(`${path}: calendar.google.bookingPageUrl must map env names to HTTPS URLs or null`);
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 assertOnly(value2, allowed, label) {
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 isRecord5(value2) {
1360
+ function isRecord6(value2) {
1334
1361
  return value2 !== null && typeof value2 === "object" && !Array.isArray(value2);
1335
1362
  }
1336
- function safeText2(value2, max) {
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 safeText3(res)}`);
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 safeText3(res)}`);
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 safeText3(res) {
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 (!isRecord6(schema) || !isRecord6(schema.entities)) return false;
4163
+ if (!isRecord7(schema) || !isRecord7(schema.entities)) return false;
4137
4164
  const entity = schema.entities[ns];
4138
- if (!isRecord6(entity) || !isRecord6(entity.attrs)) return false;
4165
+ if (!isRecord7(entity) || !isRecord7(entity.attrs)) return false;
4139
4166
  const definition = entity.attrs[attr];
4140
- return isRecord6(definition) && definition.unique === true;
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 (!isRecord6(value2) || !isRecord6(value2.entities)) {
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 && !isRecord6(value2.links)) throw new Error("db schema links must be an object");
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 isRecord6(value2) {
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 ?? "not configured" : "not enabled"}`);
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") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
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 ?? "anthropic";
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
- ai: {
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: ${provider}`);
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 safeText4(res)}`);
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 safeText4(res)}`);
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 safeText4(res) {
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 = isRecord7(payload) && isRecord7(payload.result) ? payload.result[seed.ns] : void 0;
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 isRecord7(value2) {
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 safeText5(created)}`);
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 safeText5(res)}`);
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 safeText5(res)}`);
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 safeText5(res) {
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 ?? "not configured" : "not enabled"}`);
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.setService(cfg.app.id, "ai", true, { env });
10978
- out.log(`${env}: ai enabled`);
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;