@odla-ai/cli 0.10.0 → 0.10.2

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.js CHANGED
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runCli
4
- } from "./chunk-WDKBW4BE.js";
4
+ } from "./chunk-MWVKOIGR.js";
5
5
 
6
6
  // src/bin.ts
7
7
  runCli().catch((err) => {
@@ -176,14 +176,16 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
176
176
  return cached.token;
177
177
  }
178
178
  const browser = approvalBrowser(options);
179
+ const email = handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0);
179
180
  const { token, expiresAt } = await requestToken({
180
181
  endpoint: cfg.platformUrl,
182
+ email,
181
183
  label: `${cfg.app.id} provisioner`,
182
184
  fetch: doFetch,
183
- onCode: async ({ userCode, expiresIn }) => {
184
- const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
185
+ onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
186
+ const approvalUrl = verificationUriComplete ?? handshakeUrl(cfg.platformUrl, userCode);
185
187
  out.log("");
186
- out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
188
+ out.log(`Review, then approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
187
189
  if (browser.open) {
188
190
  try {
189
191
  await (options.openApprovalUrl ?? openUrl)(approvalUrl);
@@ -197,10 +199,17 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
197
199
  out.log("");
198
200
  }
199
201
  });
200
- writePrivateJson(cfg.local.tokenFile, { platform: audience, token, expiresAt });
202
+ writePrivateJson(cfg.local.tokenFile, { platform: audience, email, token, expiresAt });
201
203
  out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
202
204
  return token;
203
205
  }
206
+ function handshakeEmail(value, cached) {
207
+ const email = (value ?? process3.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
208
+ if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
209
+ throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
210
+ }
211
+ return email;
212
+ }
204
213
  function approvalBrowser(options) {
205
214
  if (options.open === true) return { open: true, mode: "forced" };
206
215
  if (options.open === false) return { open: false, reason: "disabled by --no-open" };
@@ -267,14 +276,16 @@ async function scopedToken(platform, scope, options, doFetch, out) {
267
276
  out.log(`auth: using cached ${scope} grant (${tokenFile})`);
268
277
  return cached.token;
269
278
  }
279
+ const email = handshakeEmail(options.email, cache?.platform === audience ? cache.email : void 0);
270
280
  const { token, expiresAt } = await requestToken2({
271
281
  endpoint: audience,
282
+ email,
272
283
  label: `odla CLI admin AI (${scope})`,
273
284
  scopes: [scope],
274
285
  fetch: doFetch,
275
- onCode: async ({ userCode, expiresIn }) => {
276
- const approvalUrl = handshakeUrl(audience, userCode);
277
- out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
286
+ onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
287
+ const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
288
+ out.log(`Review, then approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
278
289
  const shouldOpen = options.open === true || options.open !== false && !process4.env.CI && Boolean(process4.stdin.isTTY && process4.stdout.isTTY);
279
290
  if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
280
291
  }
@@ -282,7 +293,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
282
293
  const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
283
294
  tokens[scope] = { token, expiresAt };
284
295
  if (existsSync2(`${process4.cwd()}/.git`)) ensureGitignore(process4.cwd(), [tokenFile]);
285
- writePrivateJson(tokenFile, { platform: audience, tokens });
296
+ writePrivateJson(tokenFile, { platform: audience, email, tokens });
286
297
  out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
287
298
  return token;
288
299
  }
@@ -478,6 +489,7 @@ async function adminAi(options) {
478
489
  platform,
479
490
  scope,
480
491
  token: options.token,
492
+ email: options.email,
481
493
  open: options.open,
482
494
  fetch: doFetch,
483
495
  stdout: out,
@@ -649,6 +661,7 @@ function isRecord3(value) {
649
661
  // src/capabilities.ts
650
662
  var CAPABILITIES = {
651
663
  cli: [
664
+ "start an email-bound device request without accepting a password or user session token",
652
665
  "register the app and enable configured services per environment",
653
666
  "issue, persist, and explicitly rotate configured service credentials",
654
667
  "write local Worker values and push deployed Worker secrets through Wrangler stdin",
@@ -667,7 +680,7 @@ var CAPABILITIES = {
667
680
  "wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
668
681
  ],
669
682
  human: [
670
- "approve the odla device code and one-off third-party logins",
683
+ "provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
671
684
  "review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
672
685
  "consent to production changes with --yes",
673
686
  "request destructive credential rotation explicitly",
@@ -677,6 +690,7 @@ var CAPABILITIES = {
677
690
  ],
678
691
  studio: [
679
692
  "view telemetry and environment state",
693
+ "let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
680
694
  "review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
681
695
  "perform manual credential recovery when the CLI's local shown-once copy is unavailable",
682
696
  "configure system AI purposes and view app/environment/run-attributed usage",
@@ -1210,6 +1224,7 @@ async function lifecycleContext(options) {
1210
1224
  {
1211
1225
  configPath: cfg.configPath,
1212
1226
  token: options.token,
1227
+ email: options.email,
1213
1228
  open: options.open,
1214
1229
  interactive: options.interactive,
1215
1230
  openApprovalUrl: options.openConsentUrl
@@ -2970,6 +2985,7 @@ async function smoke(options) {
2970
2985
  {
2971
2986
  configPath: cfg.configPath,
2972
2987
  token: options.token,
2988
+ email: options.email,
2973
2989
  open: options.open,
2974
2990
  interactive: options.interactive,
2975
2991
  openApprovalUrl: options.openApprovalUrl
@@ -3146,6 +3162,7 @@ async function adminCommand(parsed) {
3146
3162
  "platform",
3147
3163
  "json",
3148
3164
  "open",
3165
+ "email",
3149
3166
  "provider",
3150
3167
  "model",
3151
3168
  "enabled",
@@ -3173,6 +3190,7 @@ async function adminCommand(parsed) {
3173
3190
  maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
3174
3191
  maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
3175
3192
  platform: stringOpt(parsed.options.platform),
3193
+ email: stringOpt(parsed.options.email),
3176
3194
  json: parsed.options.json === true,
3177
3195
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3178
3196
  credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
@@ -3202,13 +3220,13 @@ Usage:
3202
3220
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
3203
3221
  odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
3204
3222
  odla-ai doctor [--config odla.config.mjs]
3205
- odla-ai calendar status [--env dev] [--json]
3206
- odla-ai calendar calendars [--env dev] [--json]
3207
- odla-ai calendar connect [--env dev] [--no-open] [--yes]
3208
- odla-ai calendar resync [--env dev] [--yes]
3209
- odla-ai calendar disconnect [--env dev] --yes
3223
+ odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
3224
+ odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
3225
+ odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
3226
+ odla-ai calendar resync [--env dev] [--email <odla-account>] [--yes]
3227
+ odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
3210
3228
  odla-ai capabilities [--json]
3211
- odla-ai admin ai show [--platform https://odla.ai] [--json]
3229
+ odla-ai admin ai show [--platform https://odla.ai] [--email <odla-account>] [--json]
3212
3230
  odla-ai admin ai models [--provider <id>] [--json]
3213
3231
  odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
3214
3232
  odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
@@ -3217,7 +3235,7 @@ Usage:
3217
3235
  odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
3218
3236
  odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
3219
3237
  odla-ai admin ai audit [--limit <1-200>] [--json]
3220
- odla-ai security github connect [--repo owner/name] [--env dev] [--no-open]
3238
+ odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
3221
3239
  odla-ai security github disconnect --source <id> [--env dev] [--yes]
3222
3240
  odla-ai security plan [--env dev] [--json]
3223
3241
  odla-ai security sources [--env dev] [--json]
@@ -3226,12 +3244,12 @@ Usage:
3226
3244
  odla-ai security report <job-id> [--json]
3227
3245
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
3228
3246
  odla-ai security run [target] --self --ack-redacted-source
3229
- odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
3230
- odla-ai smoke [--config odla.config.mjs] [--env dev] [--no-open]
3247
+ odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
3248
+ odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
3231
3249
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
3232
3250
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
3233
- odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
3234
- odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
3251
+ odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
3252
+ odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
3235
3253
  odla-ai version
3236
3254
 
3237
3255
  Commands:
@@ -3259,6 +3277,10 @@ Safety:
3259
3277
  preflights Wrangler before any shown-once issuance or destructive rotation.
3260
3278
  Provision opens the approval page automatically in interactive terminals;
3261
3279
  use --open to force browser launch or --no-open to suppress it.
3280
+ A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
3281
+ The email is a non-secret identity hint: never provide a password or session
3282
+ token. The matching account must already exist, be signed in, explicitly
3283
+ review the exact code, and finish any current request before claiming another.
3262
3284
  Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
3263
3285
  OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
3264
3286
  GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
@@ -3308,7 +3330,7 @@ async function hostedSecurityContext(parsed, dependencies) {
3308
3330
  const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
3309
3331
  const token = await getDeveloperToken(
3310
3332
  cfg,
3311
- { configPath, open, openApprovalUrl: dependencies.openUrl },
3333
+ { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
3312
3334
  doFetch,
3313
3335
  stdout
3314
3336
  );
@@ -3454,6 +3476,7 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
3454
3476
  "source",
3455
3477
  "ref",
3456
3478
  "follow",
3479
+ "email",
3457
3480
  "open",
3458
3481
  "json",
3459
3482
  "ack-redacted-source",
@@ -3542,6 +3565,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
3542
3565
  "platform",
3543
3566
  "self",
3544
3567
  "ack-redacted-source",
3568
+ "email",
3545
3569
  "open",
3546
3570
  "fail-on",
3547
3571
  "fail-on-candidates",
@@ -3571,6 +3595,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
3571
3595
  return getScopedPlatformToken({
3572
3596
  platform: request.platform,
3573
3597
  scope: request.scope,
3598
+ email: stringOpt(parsed.options.email),
3574
3599
  open,
3575
3600
  fetch: doFetch,
3576
3601
  stdout: out,
@@ -3583,7 +3608,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
3583
3608
  }
3584
3609
  return getDeveloperToken(
3585
3610
  cfg,
3586
- { configPath, open, openApprovalUrl: dependencies.openUrl },
3611
+ { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
3587
3612
  doFetch,
3588
3613
  out
3589
3614
  );
@@ -3617,7 +3642,7 @@ async function securityCommand(parsed, dependencies) {
3617
3642
  return;
3618
3643
  }
3619
3644
  if (sub === "plan") {
3620
- assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
3645
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
3621
3646
  const context = await hostedSecurityContext(parsed, dependencies);
3622
3647
  const plan = await getHostedSecurityPlan(context);
3623
3648
  if (parsed.options.json === true) context.stdout.log(JSON.stringify(plan, null, 2));
@@ -3633,7 +3658,7 @@ async function securityCommand(parsed, dependencies) {
3633
3658
  return;
3634
3659
  }
3635
3660
  if (sub === "report") {
3636
- assertArgs(parsed, ["config", "env", "platform", "open", "json"], 3);
3661
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
3637
3662
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
3638
3663
  const context = await hostedSecurityContext(parsed, dependencies);
3639
3664
  const report = await getHostedSecurityReport({ ...context, jobId });
@@ -3651,7 +3676,7 @@ async function securityCommand(parsed, dependencies) {
3651
3676
  async function githubSecurityCommand(parsed, dependencies) {
3652
3677
  const action = parsed.positionals[2];
3653
3678
  if (action === "disconnect") {
3654
- assertArgs(parsed, ["config", "env", "platform", "source", "open", "yes"], 3);
3679
+ assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
3655
3680
  const context2 = await hostedSecurityContext(parsed, dependencies);
3656
3681
  const sourceId = requiredString(parsed.options.source, "--source");
3657
3682
  const confirmed = parsed.options.yes === true || await interactiveConfirmation(
@@ -3668,7 +3693,7 @@ async function githubSecurityCommand(parsed, dependencies) {
3668
3693
  if (action !== "connect") {
3669
3694
  throw new Error('unknown security github command. Try "odla-ai security github connect".');
3670
3695
  }
3671
- assertArgs(parsed, ["config", "env", "platform", "repo", "open"], 3);
3696
+ assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
3672
3697
  const context = await hostedSecurityContext(parsed, dependencies);
3673
3698
  const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin);
3674
3699
  const connection = await connectGitHubSecuritySource({
@@ -3683,7 +3708,7 @@ async function githubSecurityCommand(parsed, dependencies) {
3683
3708
  context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
3684
3709
  }
3685
3710
  async function listSecuritySources(parsed, dependencies) {
3686
- assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
3711
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
3687
3712
  const context = await hostedSecurityContext(parsed, dependencies);
3688
3713
  const [plan, sources] = await Promise.all([
3689
3714
  getHostedSecurityPlan(context),
@@ -3705,7 +3730,7 @@ source repository default ref status`);
3705
3730
  }
3706
3731
  }
3707
3732
  async function securityStatus(parsed, dependencies) {
3708
- assertArgs(parsed, ["config", "env", "platform", "open", "json", "follow"], 3);
3733
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json", "follow"], 3);
3709
3734
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
3710
3735
  const context = await hostedSecurityContext(parsed, dependencies);
3711
3736
  const job = parsed.options.follow === true ? await followHostedSecurityJob({
@@ -3782,11 +3807,12 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3782
3807
  return;
3783
3808
  }
3784
3809
  if (command === "smoke") {
3785
- assertArgs(parsed, ["config", "env", "token", "open"], 1);
3810
+ assertArgs(parsed, ["config", "env", "token", "email", "open"], 1);
3786
3811
  await smoke({
3787
3812
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3788
3813
  env: stringOpt(parsed.options.env),
3789
3814
  token: stringOpt(parsed.options.token),
3815
+ email: stringOpt(parsed.options.email),
3790
3816
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3791
3817
  openApprovalUrl: dependencies.openUrl,
3792
3818
  fetch: dependencies.fetch,
@@ -3824,7 +3850,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3824
3850
  if (command === "secrets") {
3825
3851
  const sub = parsed.positionals[1];
3826
3852
  if (sub === "set" || sub === "set-clerk-key") {
3827
- assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "yes"], sub === "set" ? 3 : 2);
3853
+ assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "email", "yes"], sub === "set" ? 3 : 2);
3828
3854
  const options = {
3829
3855
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3830
3856
  name: parsed.positionals[2],
@@ -3832,6 +3858,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3832
3858
  fromEnv: stringOpt(parsed.options["from-env"]),
3833
3859
  stdin: parsed.options.stdin === true,
3834
3860
  token: stringOpt(parsed.options.token),
3861
+ email: stringOpt(parsed.options.email),
3835
3862
  yes: parsed.options.yes === true,
3836
3863
  fetch: dependencies.fetch,
3837
3864
  stdout: dependencies.stdout
@@ -3865,6 +3892,7 @@ async function provisionCommand(parsed, dependencies) {
3865
3892
  "write-credentials",
3866
3893
  "write-dev-vars",
3867
3894
  "token",
3895
+ "email",
3868
3896
  "open",
3869
3897
  "yes"
3870
3898
  ], 1);
@@ -3878,6 +3906,7 @@ async function provisionCommand(parsed, dependencies) {
3878
3906
  writeCredentials: parsed.options["write-credentials"] !== false,
3879
3907
  writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
3880
3908
  token: stringOpt(parsed.options.token),
3909
+ email: stringOpt(parsed.options.email),
3881
3910
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3882
3911
  yes: parsed.options.yes === true,
3883
3912
  fetch: dependencies.fetch,
@@ -3891,13 +3920,14 @@ async function calendarCommand(parsed, dependencies) {
3891
3920
  if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "resync" && sub !== "disconnect") {
3892
3921
  throw new Error(`unknown calendar subcommand "${sub ?? ""}". Try "odla-ai calendar status --env dev".`);
3893
3922
  }
3894
- assertArgs(parsed, ["config", "env", "json", "token", "open", "yes"], 2);
3923
+ assertArgs(parsed, ["config", "env", "json", "token", "email", "open", "yes"], 2);
3895
3924
  if (sub !== "status" && sub !== "calendars" && parsed.options.json !== void 0) throw new Error(`--json is supported only by calendar status/calendars`);
3896
3925
  if ((sub === "status" || sub === "calendars") && parsed.options.yes !== void 0) throw new Error(`--yes is not used by "calendar ${sub}"`);
3897
3926
  const options = {
3898
3927
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3899
3928
  env: stringOpt(parsed.options.env),
3900
3929
  token: stringOpt(parsed.options.token),
3930
+ email: stringOpt(parsed.options.email),
3901
3931
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3902
3932
  yes: parsed.options.yes === true,
3903
3933
  json: parsed.options.json === true,
@@ -3952,4 +3982,4 @@ export {
3952
3982
  smoke,
3953
3983
  runCli
3954
3984
  };
3955
- //# sourceMappingURL=chunk-WDKBW4BE.js.map
3985
+ //# sourceMappingURL=chunk-MWVKOIGR.js.map