@odla-ai/cli 0.2.1 → 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/bin.cjs CHANGED
@@ -146,6 +146,7 @@ var REPLACEMENTS = [
146
146
  [/\bsk_(live|test)_[A-Za-z0-9]+/g, "sk_$1_[redacted]"],
147
147
  [/\bsk-[A-Za-z0-9._-]+/g, "sk-[redacted]"],
148
148
  [/\bwhsec_[A-Za-z0-9+/=]+/g, "whsec_[redacted]"],
149
+ [/\bo11y_[A-Za-z0-9]+/g, "o11y_[redacted]"],
149
150
  [/\b(ghp|gho|github_pat)_[A-Za-z0-9_]+/g, "$1_[redacted]"],
150
151
  [/\bAKIA[A-Z0-9]{12,}/g, "AKIA[redacted]"]
151
152
  ];
@@ -290,7 +291,7 @@ function wranglerWarnings(rootDir) {
290
291
  const vars = block.vars;
291
292
  if (vars && typeof vars === "object") {
292
293
  for (const [name, value] of Object.entries(vars)) {
293
- if (name === "ODLA_API_KEY" || typeof value === "string" && looksSecret(value)) {
294
+ if (name === "ODLA_API_KEY" || name === "ODLA_O11Y_TOKEN" || typeof value === "string" && looksSecret(value)) {
294
295
  warnings.push(`wrangler ${label}vars.${name} looks like a secret \u2014 use "odla-ai secrets push" / wrangler secret put, never vars`);
295
296
  }
296
297
  }
@@ -310,54 +311,6 @@ function readPackageJson(rootDir) {
310
311
  }
311
312
  }
312
313
 
313
- // src/doctor.ts
314
- async function doctor(options) {
315
- const out = options.stdout ?? console;
316
- const cfg = await loadProjectConfig(options.configPath);
317
- const plan = buildPlan(cfg);
318
- const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
319
- const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
320
- const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
321
- const entities = serializedEntities(schema);
322
- out.log(`config: ${cfg.configPath}`);
323
- out.log(`app: ${plan.appName} (${plan.appId})`);
324
- out.log(`envs: ${plan.envs.join(", ")}`);
325
- out.log(`svc: ${plan.services.join(", ")}`);
326
- out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
327
- out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
328
- out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
329
- const warnings = [];
330
- if (schema && entities.length === 0) warnings.push("schema has no entities");
331
- if (rules) {
332
- for (const ns of Object.keys(rules)) {
333
- if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
334
- }
335
- }
336
- if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
337
- if (cfg.auth?.clerk) {
338
- for (const [env, value] of Object.entries(cfg.auth.clerk)) {
339
- if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
340
- warnings.push(`auth.clerk.${env} references unset env var ${value}`);
341
- }
342
- }
343
- }
344
- if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
345
- warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
346
- warnings.push(...trackedSecretFiles(cfg.rootDir));
347
- warnings.push(...wranglerWarnings(cfg.rootDir));
348
- if (warnings.length) {
349
- out.log("");
350
- out.log("warnings:");
351
- for (const warning of warnings) out.log(` - ${warning}`);
352
- } else {
353
- out.log("ok");
354
- }
355
- }
356
-
357
- // src/init.ts
358
- var import_node_fs5 = require("fs");
359
- var import_node_path5 = require("path");
360
-
361
314
  // src/local.ts
362
315
  var import_node_fs4 = require("fs");
363
316
  var import_node_path4 = require("path");
@@ -393,7 +346,8 @@ function mergeCredential(current, update) {
393
346
  next.envs[update.env] = {
394
347
  ...next.envs[update.env] ?? {},
395
348
  tenantId: update.tenantId,
396
- ...update.dbKey ? { dbKey: update.dbKey } : {}
349
+ ...update.dbKey ? { dbKey: update.dbKey } : {},
350
+ ...update.o11yToken ? { o11yToken: update.o11yToken } : {}
397
351
  };
398
352
  return next;
399
353
  }
@@ -406,7 +360,7 @@ function ensureGitignore(rootDir) {
406
360
  (0, import_node_fs4.writeFileSync)(path, `${existing}${prefix}${missing.join("\n")}
407
361
  `);
408
362
  }
409
- function writeDevVars(path, credentials, env) {
363
+ function writeDevVars(path, credentials, env, o11y) {
410
364
  const entry = credentials.envs[env];
411
365
  if (!entry?.dbKey) throw new Error(`no db key for env "${env}" in ${path}`);
412
366
  const lines = [
@@ -417,6 +371,11 @@ function writeDevVars(path, credentials, env) {
417
371
  `ODLA_TENANT="${entry.tenantId}"`,
418
372
  `ODLA_API_KEY="${entry.dbKey}"`
419
373
  ];
374
+ if (o11y) {
375
+ lines.push(`ODLA_O11Y_ENDPOINT="${o11y.endpoint}"`, `ODLA_O11Y_SERVICE="${o11y.service}"`);
376
+ if (o11y.version) lines.push(`ODLA_O11Y_VERSION="${o11y.version}"`);
377
+ if (entry.o11yToken) lines.push(`ODLA_O11Y_TOKEN="${entry.o11yToken}"`);
378
+ }
420
379
  (0, import_node_fs4.mkdirSync)((0, import_node_path4.dirname)(path), { recursive: true });
421
380
  (0, import_node_fs4.writeFileSync)(path, `${lines.join("\n")}
422
381
  `);
@@ -427,7 +386,61 @@ function displayPath(path, rootDir = process.cwd()) {
427
386
  return rel && !rel.startsWith("..") ? rel : path;
428
387
  }
429
388
 
389
+ // src/doctor.ts
390
+ async function doctor(options) {
391
+ const out = options.stdout ?? console;
392
+ const cfg = await loadProjectConfig(options.configPath);
393
+ const plan = buildPlan(cfg);
394
+ const schema = await resolveDataExport(cfg, cfg.db?.schema, ["schema", "SCHEMA"]);
395
+ const configuredRules = await resolveDataExport(cfg, cfg.db?.rules, ["rules", "RULES"]);
396
+ const rules = configuredRules ?? (schema && cfg.db?.defaultRules !== false ? rulesFromSchema(schema) : void 0);
397
+ const entities = serializedEntities(schema);
398
+ out.log(`config: ${cfg.configPath}`);
399
+ out.log(`app: ${plan.appName} (${plan.appId})`);
400
+ out.log(`envs: ${plan.envs.join(", ")}`);
401
+ out.log(`svc: ${plan.services.join(", ")}`);
402
+ out.log(`schema: ${schema ? `${entities.length} entities` : "none"}`);
403
+ out.log(`rules: ${rules ? `${Object.keys(rules).length} namespaces` : "none"}`);
404
+ out.log(`ai: ${cfg.ai?.provider ?? "not configured"}`);
405
+ const warnings = [];
406
+ if (schema && entities.length === 0) warnings.push("schema has no entities");
407
+ if (rules) {
408
+ for (const ns of Object.keys(rules)) {
409
+ if (entities.length > 0 && !entities.includes(ns)) warnings.push(`rules include "${ns}" but schema has no matching entity`);
410
+ }
411
+ }
412
+ if (cfg.services.includes("ai") && !cfg.ai?.provider) warnings.push("ai service is enabled but ai.provider is not set");
413
+ if (cfg.auth?.clerk) {
414
+ for (const [env, value] of Object.entries(cfg.auth.clerk)) {
415
+ if (typeof value === "string" && value.startsWith("$") && !process.env[value.slice(1)]) {
416
+ warnings.push(`auth.clerk.${env} references unset env var ${value}`);
417
+ }
418
+ }
419
+ }
420
+ if (cfg.ai?.keyEnv && !process.env[cfg.ai.keyEnv]) warnings.push(`${cfg.ai.keyEnv} is not set; provision will skip provider key storage`);
421
+ if (cfg.services.includes("o11y")) {
422
+ const credentials = readCredentials(cfg.local.credentialsFile);
423
+ for (const env of cfg.envs) {
424
+ if (!credentials?.envs[env]?.o11yToken) {
425
+ warnings.push(`o11y is enabled but env "${env}" has no ingest token \u2014 run "odla-ai provision" to mint one`);
426
+ }
427
+ }
428
+ }
429
+ warnings.push(...lintRules(rules, entities, cfg.db?.publicRead ?? []));
430
+ warnings.push(...trackedSecretFiles(cfg.rootDir));
431
+ warnings.push(...wranglerWarnings(cfg.rootDir));
432
+ if (warnings.length) {
433
+ out.log("");
434
+ out.log("warnings:");
435
+ for (const warning of warnings) out.log(` - ${warning}`);
436
+ } else {
437
+ out.log("ok");
438
+ }
439
+ }
440
+
430
441
  // src/init.ts
442
+ var import_node_fs5 = require("fs");
443
+ var import_node_path5 = require("path");
431
444
  function initProject(options) {
432
445
  const out = options.stdout ?? console;
433
446
  const rootDir = (0, import_node_path5.resolve)(options.rootDir ?? process.cwd());
@@ -484,6 +497,12 @@ function configTemplate(input) {
484
497
  // prod: "$CLERK_PUBLISHABLE_KEY",
485
498
  },
486
499
  },
500
+ // Add "o11y" to services to enable observability; provision then mints the
501
+ // ingest token and scaffolds the ODLA_O11Y_* vars into .dev.vars.
502
+ // o11y: {
503
+ // service: "${input.appId}", // defaults to the app id
504
+ // // endpoint: "https://o11y.odla.ai",
505
+ // },
487
506
  links: {
488
507
  // dev: "https://dev.example.com",
489
508
  // prod: "https://example.com",
@@ -684,6 +703,15 @@ async function provision(options) {
684
703
  dbKey = await mintDbKey({ cfg, tenantId, env, token, auth, fetch: doFetch });
685
704
  out.log(`${env}: minted db key for ${tenantId}`);
686
705
  }
706
+ let o11yToken = cfg.services.includes("o11y") && !options.rotateKeys ? credentials?.envs[env]?.o11yToken : void 0;
707
+ if (cfg.services.includes("o11y")) {
708
+ if (o11yToken) {
709
+ out.log(`${env}: reusing local o11y ingest token`);
710
+ } else {
711
+ o11yToken = await mintO11yToken(cfg, env, token, doFetch);
712
+ out.log(`${env}: minted o11y ingest token`);
713
+ }
714
+ }
687
715
  if (schema) {
688
716
  await postJson(doFetch, `${cfg.dbEndpoint}/app/${encodeURIComponent(tenantId)}/schema`, dbKey, { schema });
689
717
  out.log(`${env}: schema pushed`);
@@ -708,7 +736,8 @@ async function provision(options) {
708
736
  dbEndpoint: cfg.dbEndpoint,
709
737
  env,
710
738
  tenantId,
711
- dbKey
739
+ dbKey,
740
+ ...o11yToken ? { o11yToken } : {}
712
741
  });
713
742
  }
714
743
  if (cfg.local.gitignore) ensureGitignore(cfg.rootDir);
@@ -719,10 +748,18 @@ async function provision(options) {
719
748
  const devVarsTarget = resolveWriteDevVarsTarget(cfg, options.writeDevVars);
720
749
  if (devVarsTarget && credentials) {
721
750
  const env = cfg.envs.includes("dev") ? "dev" : cfg.envs[0] ?? "prod";
722
- writeDevVars(devVarsTarget, credentials, env);
751
+ writeDevVars(devVarsTarget, credentials, env, o11yDevVars(cfg));
723
752
  out.log(`dev vars: wrote ${displayPath(devVarsTarget, cfg.rootDir)} for ${env}`);
724
753
  }
725
754
  }
755
+ function o11yDevVars(cfg) {
756
+ if (!cfg.services.includes("o11y")) return void 0;
757
+ return {
758
+ endpoint: cfg.o11y?.endpoint ?? "https://o11y.odla.ai",
759
+ service: cfg.o11y?.service ?? cfg.app.id,
760
+ ...cfg.o11y?.version ? { version: cfg.o11y.version } : {}
761
+ };
762
+ }
726
763
  async function readRegistryApp(cfg, token, doFetch) {
727
764
  const res = await doFetch(`${cfg.platformUrl}/registry/apps/${encodeURIComponent(cfg.app.id)}`, {
728
765
  headers: { authorization: `Bearer ${token}` }
@@ -759,6 +796,16 @@ async function mintDbKey(opts) {
759
796
  if (!body.key) throw new Error(`db key mint (${opts.tenantId}) returned no key`);
760
797
  return body.key;
761
798
  }
799
+ async function mintO11yToken(cfg, env, token, doFetch) {
800
+ const res = await doFetch(
801
+ `${cfg.platformUrl}/o11y/${encodeURIComponent(cfg.app.id)}/token?env=${encodeURIComponent(env)}`,
802
+ { method: "POST", headers: { authorization: `Bearer ${token}` } }
803
+ );
804
+ if (!res.ok) throw new Error(`o11y token mint (${env}) failed: ${res.status} ${await safeText(res)}`);
805
+ const body = await res.json();
806
+ if (!body.token) throw new Error(`o11y token mint (${env}) returned no token`);
807
+ return body.token;
808
+ }
762
809
  async function postJson(doFetch, url, bearer, body) {
763
810
  const res = await doFetch(url, {
764
811
  method: "POST",
@@ -821,19 +868,24 @@ async function secretsPush(options) {
821
868
  }
822
869
  const wranglerEnv = PROD_ENV_NAMES.has(env) ? void 0 : env;
823
870
  const target = wranglerEnv ? `wrangler env "${wranglerEnv}"` : "the top-level (prod) wrangler env";
871
+ const secrets = [{ name: "ODLA_API_KEY", value: dbKey }];
872
+ const o11yToken = credentials.envs[env]?.o11yToken;
873
+ if (o11yToken) secrets.push({ name: "ODLA_O11Y_TOKEN", value: o11yToken });
824
874
  if (options.dryRun) {
825
- out.log(`dry run: would push ODLA_API_KEY (${redactSecrets(dbKey)}) to ${target}`);
875
+ for (const s of secrets) out.log(`dry run: would push ${s.name} (${redactSecrets(s.value)}) to ${target}`);
826
876
  return;
827
877
  }
828
878
  const run = options.runner ?? defaultRunner;
829
879
  if (!await wranglerLoggedIn(run, cfg.rootDir)) {
830
880
  throw new Error(`wrangler is not logged in \u2014 run "wrangler login" (a browser step for the human)`);
831
881
  }
832
- const result = await wranglerPutSecret(run, { name: "ODLA_API_KEY", value: dbKey, env: wranglerEnv, cwd: cfg.rootDir });
833
- if (result.code !== 0) {
834
- throw new Error(`wrangler secret put failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
882
+ for (const s of secrets) {
883
+ const result = await wranglerPutSecret(run, { name: s.name, value: s.value, env: wranglerEnv, cwd: cfg.rootDir });
884
+ if (result.code !== 0) {
885
+ throw new Error(`wrangler secret put ${s.name} failed (exit ${result.code}): ${redactSecrets(`${result.stderr || result.stdout}`.trim())}`);
886
+ }
887
+ out.log(`${s.name} pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
835
888
  }
836
- out.log(`ODLA_API_KEY pushed to ${target} (value read from ${credentialsPath}, never echoed)`);
837
889
  }
838
890
 
839
891
  // src/skill.ts