@odla-ai/cli 0.2.0 → 0.3.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/dist/index.cjs CHANGED
@@ -165,6 +165,7 @@ var REPLACEMENTS = [
165
165
  [/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
166
166
  [/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
167
167
  [/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
168
+ [/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
168
169
  [/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
169
170
  [/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
170
171
  ];
@@ -309,7 +310,7 @@ function wranglerWarnings(rootDir) {
309
310
  const vars = block.vars;
310
311
  if (vars && typeof vars === "object") {
311
312
  for (const [name, value] of Object.entries(vars)) {
312
- if (name === "ODLA_API_KEY" || typeof value === "string" && looksSecret(value)) {
313
+ if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value === "string" && looksSecret(value)) {
313
314
  warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
314
315
  }
315
316
  }
@@ -329,54 +330,6 @@ function readPackageJson(rootDir) {
329
330
  }
330
331
  }
331
332
 
332
- // src/doctor.ts
333
- async function doctor(options) {
334
- const out = options.stdout ?? console;
335
- const cfg = await loadProjectConfig(options.configPath);
336
- const plan = buildPlan(cfg);
337
- const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
338
- const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
339
- const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
340
- const entities = serializedEntities(schema);
341
- out.log(`config: ${cfg.configPath}`);
342
- out.log(`app: ${plan.appName} (${plan.appId})`);
343
- out.log(`envs: ${plan.envs.join(", ")}`);
344
- out.log(`svc: ${plan.services.join(", ")}`);
345
- out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
346
- out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
347
- out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
348
- const warnings = [];
349
- if (schema && entities.length === 0) warnings.push("schema has no entities");
350
- if (rules) {
351
- for (const ns of Object.keys(rules)) {
352
- if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
353
- }
354
- }
355
- if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
356
- if (cfg.auth?.clerk) {
357
- for (const [env, value] of Object.entries(cfg.auth.clerk)) {
358
- if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
359
- warnings.push(`auth.clerk.${env} references unset env var ${value}`);
360
- }
361
- }
362
- }
363
- if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
364
- warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
365
- warnings.push(...trackedSecretFiles(cfg.rootDir));
366
- warnings.push(...wranglerWarnings(cfg.rootDir));
367
- if (warnings.length) {
368
- out.log("");
369
- out.log("warnings:");
370
- for (const warning of warnings) out.log(` - ${warning}`);
371
- } else {
372
- out.log("ok");
373
- }
374
- }
375
-
376
- // src/init.ts
377
- var import_node_fs5 = require("fs");
378
- var import_node_path5 = require("path");
379
-
380
333
  // src/local.ts
381
334
  var import_node_fs4 = require("fs");
382
335
  var import_node_path4 = require("path");
@@ -412,7 +365,8 @@ function mergeCredential(current, update) {
412
365
  next.envs[update.env] = {
413
366
  ...next.envs[update.env] ?? {},
414
367
  tenantId: update.tenantId,
415
- ...update.dbKey ? { dbKey: update.dbKey } : {}
368
+ ...update.dbKey ? { dbKey: update.dbKey } : {},
369
+ ...update.o11yToken ? { o11yToken: update.o11yToken } : {}
416
370
  };
417
371
  return next;
418
372
  }
@@ -425,7 +379,7 @@ function ensureGitignore(rootDir) {
425
379
  (0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
426
380
  `);
427
381
  }
428
- function writeDevVars(path, credentials, env) {
382
+ function writeDevVars(path, credentials, env, o11y) {
429
383
  const entry = credentials.envs[env];
430
384
  if (!entry?.dbKey) throw new Error(`no db key for env "${env}" in ${path}`);
431
385
  const lines = [
@@ -436,6 +390,11 @@ function writeDevVars(path, credentials, env) {
436
390
  `ODLA_TENANT="${entry.tenantId}"`,
437
391
  `ODLA_API_KEY="${entry.dbKey}"`
438
392
  ];
393
+ if (o11y) {
394
+ lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
395
+ if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
396
+ if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
397
+ }
439
398
  (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
440
399
  (0, import_node_fs4.writeFileSync)(path, `${lines.join("\n")}
441
400
  `);
@@ -446,7 +405,61 @@ function displayPath(path, rootDir = process.cwd()) {
446
405
  return rel && !rel.startsWith("..") ? rel : path;
447
406
  }
448
407
 
408
+ // src/doctor.ts
409
+ async function doctor(options) {
410
+ const out = options.stdout ?? console;
411
+ const cfg = await loadProjectConfig(options.configPath);
412
+ const plan = buildPlan(cfg);
413
+ const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
414
+ const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
415
+ const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
416
+ const entities = serializedEntities(schema);
417
+ out.log(`config: ${cfg.configPath}`);
418
+ out.log(`app: ${plan.appName} (${plan.appId})`);
419
+ out.log(`envs: ${plan.envs.join(", ")}`);
420
+ out.log(`svc: ${plan.services.join(", ")}`);
421
+ out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
422
+ out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
423
+ out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
424
+ const warnings = [];
425
+ if (schema && entities.length === 0) warnings.push("schema has no entities");
426
+ if (rules) {
427
+ for (const ns of Object.keys(rules)) {
428
+ if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
429
+ }
430
+ }
431
+ if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
432
+ if (cfg.auth?.clerk) {
433
+ for (const [env, value] of Object.entries(cfg.auth.clerk)) {
434
+ if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
435
+ warnings.push(`auth.clerk.${env} references unset env var ${value}`);
436
+ }
437
+ }
438
+ }
439
+ if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
440
+ if (cfg.services.includes("o11y")) {
441
+ const credentials = readCredentials(cfg.local.credentialsFile);
442
+ for (const env of cfg.envs) {
443
+ if (!credentials?.envs[env]?.o11yToken) {
444
+ warnings.push(`o11y is enabled but env "${env}" has no ingest token \u2014 run "odla-ai provision" to mint one`);
445
+ }
446
+ }
447
+ }
448
+ warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
449
+ warnings.push(...trackedSecretFiles(cfg.rootDir));
450
+ warnings.push(...wranglerWarnings(cfg.rootDir));
451
+ if (warnings.length) {
452
+ out.log("");
453
+ out.log("warnings:");
454
+ for (const warning of warnings) out.log(` - ${warning}`);
455
+ } else {
456
+ out.log("ok");
457
+ }
458
+ }
459
+
449
460
  // src/init.ts
461
+ var import_node_fs5 = require("fs");
462
+ var import_node_path5 = require("path");
450
463
  function initProject(options) {
451
464
  const out = options.stdout ?? console;
452
465
  const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
@@ -503,6 +516,12 @@ function configTemplate(input) {
503
516
  // prod: "$CLERK_PUBLISHABLE_KEY",
504
517
  },
505
518
  },
519
+ // Add "o11y" to services to enable observability; provision then mints the
520
+ // ingest token and scaffolds the ODLA_O11Y_* vars into .dev.vars.
521
+ // o11y: {
522
+ // service: "${input.appId}", // defaults to the app id
523
+ // // endpoint: "https://o11y.odla.ai",
524
+ // },
506
525
  links: {
507
526
  // dev: "https://dev.example.com",
508
527
  // prod: "https://example.com",
@@ -703,6 +722,15 @@ async function provision(options) {
703
722
  dbKey = await mintDbKey({ cfg, tenantId, env, token, auth, fetch: doFetch });
704
723
  out.log(`${env}: minted db key for ${tenantId}`);
705
724
  }
725
+ let o11yToken = cfg.services.includes("o11y") && !options.rotateKeys ? credentials?.envs[env]?.o11yToken : void 0;
726
+ if (cfg.services.includes("o11y")) {
727
+ if (o11yToken) {
728
+ out.log(`${env}: reusing local o11y ingest token`);
729
+ } else {
730
+ o11yToken = await mintO11yToken(cfg, env, token, doFetch);
731
+ out.log(`${env}: minted o11y ingest token`);
732
+ }
733
+ }
706
734
  if (schema) {
707
735
  await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
708
736
  out.log(`${env}: schema pushed`);
@@ -727,7 +755,8 @@ async function provision(options) {
727
755
  dbEndpoint: cfg.dbEndpoint,
728
756
  env,
729
757
  tenantId,
730
- dbKey
758
+ dbKey,
759
+ ...o11yToken ? { o11yToken } : {}
731
760
  });
732
761
  }
733
762
  if (cfg.local.gitignore) ensureGitignore(cfg.rootDir);
@@ -738,10 +767,18 @@ async function provision(options) {
738
767
  const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
739
768
  if (devVarsTarget && credentials) {
740
769
  const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
741
- writeDevVars(devVarsTarget, credentials, env);
770
+ writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
742
771
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
743
772
  }
744
773
  }
774
+ function o11yDevVars(cfg) {
775
+ if (!cfg.services.includes("o11y")) return void 0;
776
+ return {
777
+ endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
778
+ service: cfg.o11y?.service ?? cfg.app.id,
779
+ ...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
780
+ };
781
+ }
745
782
  async function readRegistryApp(cfg, token, doFetch) {
746
783
  const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
747
784
  headers: { authorization: `Bearer ${token}` }
@@ -778,6 +815,16 @@ async function mintDbKey(opts) {
778
815
  if (!body.key) throw new Error(`db key mint (${opts.tenantId}) returned no key`);
779
816
  return body.key;
780
817
  }
818
+ async function mintO11yToken(cfg, env, token, doFetch) {
819
+ const res = await doFetch(
820
+ `${cfg.platformUrl}/o11y/${encodeURIComponent(cfg.app.id)}/token?env=${encodeURIComponent(env)}`,
821
+ { method: "POST", headers: { authorization: `Bearer ${token}` } }
822
+ );
823
+ if (!res.ok) throw new Error(`o11y token mint (${env}) failed: ${res.status} ${await safeText(res)}`);
824
+ const body = await res.json();
825
+ if (!body.token) throw new Error(`o11y token mint (${env}) returned no token`);
826
+ return body.token;
827
+ }
781
828
  async function postJson(doFetch, url, bearer, body) {
782
829
  const res = await doFetch(url, {
783
830
  method: "POST",
@@ -840,19 +887,24 @@ async function secretsPush(options) {
840
887
  }
841
888
  const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
842
889
  const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
890
+ const secrets = [{ name: "ODLA_API_KEY", value: dbKey }];
891
+ const o11yToken = credentials.envs[env]?.o11yToken;
892
+ if (o11yToken) secrets.push({ name: "ODLA_O11Y_TOKEN", value: o11yToken });
843
893
  if (options.dryRun) {
844
- out.log(`dry run: would push ODLA_API_KEY (${redactSecrets(dbKey)}) to ${target}`);
894
+ for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
845
895
  return;
846
896
  }
847
897
  const run = options.runner ?? defaultRunner;
848
898
  if (!await wranglerLoggedIn(run, cfg.rootDir)) {
849
899
  throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
850
900
  }
851
- const result = await wranglerPutSecret(run, { name: "ODLA_API_KEY", value: dbKey, env: wranglerEnv, cwd: cfg.rootDir });
852
- if (result.code !== 0) {
853
- throw new Error(`wrangler secret put failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
901
+ for (const s of secrets) {
902
+ const result = await wranglerPutSecret(run, { name: s.name, value: s.value, env: wranglerEnv, cwd: cfg.rootDir });
903
+ if (result.code !== 0) {
904
+ throw new Error(`wrangler secret put ${s.name} failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
905
+ }
906
+ out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
854
907
  }
855
- out.log(`ODLA_API_KEY pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
856
908
  }
857
909
 
858
910
  // src/skill.ts