@odla-ai/cli 0.10.0 → 0.10.1

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 CHANGED
@@ -29,13 +29,17 @@ For a project you keep working in, install it as a dev dependency so the
29
29
  shorter `npx odla-ai` form works and the version is pinned by your lockfile:
30
30
 
31
31
  ```bash
32
- npm view @odla-ai/cli@0.10.0 version
33
- npm i -D --save-exact @odla-ai/cli@0.10.0
32
+ npm view @odla-ai/cli@0.10.1 version
33
+ npm i -D --save-exact @odla-ai/cli@0.10.1
34
34
  ```
35
35
 
36
36
  ## Prerequisites
37
37
 
38
38
  - Node.js 20 or newer (`node --version`).
39
+ - An existing odla account that has signed in at least once. For any command
40
+ that may need a fresh device grant, pass `--email <account>` or set
41
+ `ODLA_USER_EMAIL`. This value identifies who may review the request; it is not
42
+ a password or session credential, and an agent must never ask for either.
39
43
  - odla apps usually deploy as Cloudflare Workers. If you have never used
40
44
  Cloudflare:
41
45
  1. Create a free account at <https://dash.cloudflare.com/sign-up>.
@@ -61,7 +65,7 @@ npx odla-ai calendar disconnect --env dev --yes
61
65
  npx odla-ai capabilities --json
62
66
  # install SDKs, write the Worker, and create wrangler.jsonc before secret push
63
67
  npx odla-ai provision --dry-run
64
- npx odla-ai provision --write-dev-vars --push-secrets
68
+ npx odla-ai provision --email owner@example.com --write-dev-vars --push-secrets
65
69
  npx odla-ai smoke --env dev
66
70
  npx odla-ai secrets push --env dev
67
71
  npx odla-ai secrets set clerk_webhook_secret --env dev --stdin
@@ -73,7 +77,7 @@ npx odla-ai security sources --env dev
73
77
  npx odla-ai security run --source <source-id> --ref main --env dev --plan-digest <digest-from-security-plan> --ack-redacted-source
74
78
  npx odla-ai security status <job-id>
75
79
  npx odla-ai security report <job-id>
76
- npx odla-ai admin ai show
80
+ npx odla-ai admin ai show --email owner@example.com
77
81
  npx odla-ai admin ai models
78
82
  npx odla-ai admin ai set security --discovery-provider anthropic --discovery-model claude-opus-4-8 --validation-provider openai --validation-model gpt-5.5 --enabled
79
83
  npx odla-ai admin ai credentials
@@ -101,8 +105,11 @@ When using `--push-secrets`, create the Worker and its Wrangler config first.
101
105
  The CLI checks that config and `wrangler whoami` before it issues or rotates a
102
106
  shown-once credential.
103
107
 
104
- 1. Gets an `odla_dev_...` token by device handshake, or reuses
105
- `ODLA_DEV_TOKEN` / `.odla/dev-token.json`.
108
+ 1. Gets an `odla_dev_...` token by email-bound device handshake, or reuses
109
+ `ODLA_DEV_TOKEN` / `.odla/dev-token.json`. A fresh handshake requires
110
+ `--email <account>` or `ODLA_USER_EMAIL`; the matching existing account must
111
+ sign in, review the exact code, and approve it. Opening the URL alone does
112
+ not claim the request.
106
113
  In an interactive terminal it opens the approval page in your browser when
107
114
  the code is displayed. Pass `--open` to force browser launch from a
108
115
  non-interactive shell, or `--no-open` to suppress it. Browser launch is
@@ -138,6 +145,26 @@ The CLI preflights Wrangler first and persists the replacement before pushing;
138
145
  if that final push still fails, follow the printed `secrets push --env ...`
139
146
  retry. Do not run the rotation flag again.
140
147
 
148
+ ### Device-grant security and recovery
149
+
150
+ The account email sent during a fresh handshake is a non-secret identity hint,
151
+ not proof of identity. Unknown or never-signed-in accounts never produce a
152
+ claimable Studio request, and the public start/poll shape intentionally does not
153
+ reveal whether an account exists. The signed-in matching user must explicitly
154
+ review the exact code before it becomes pending, and only one claimed pending
155
+ or approved-but-uncollected request can be active for that user.
156
+
157
+ Developer grants are tracked by id, owner, label, scopes, creation/expiry,
158
+ last use, and revocation state. The plaintext token is delivered once to the
159
+ polling client and is never available from the inventory. Every signed-in user
160
+ can inspect and revoke their own grants under **Your agent credentials** in
161
+ Studio; platform admins additionally see the global token inventory and
162
+ metadata-only attempt evidence. Future requests with a revoked token fail.
163
+ Deleting
164
+ `.odla/dev-token.json` or `.odla/admin-token.local.json` removes the local copy
165
+ but does not revoke a token already copied elsewhere, so use Studio revocation
166
+ when compromise or accidental approval is possible.
167
+
141
168
  `smoke` verifies a provisioned environment from local credentials. It fetches
142
169
  the platform public config, checks the configured AI provider, fetches the live
143
170
  odla-db schema with the tenant key, compares expected schema entities, and runs
package/REQUIREMENTS.md CHANGED
@@ -25,11 +25,27 @@ Agnacl, but none should mention or special-case Agnacl.
25
25
 
26
26
  - Provisioning must default to device-handshake auth, never a platform admin
27
27
  secret in chat or config.
28
- - The approval code must be printed in a copyable URL.
28
+ - A fresh handshake must require the existing account email via `--email` or
29
+ `ODLA_USER_EMAIL`. Email is an identity hint, never a password or bearer
30
+ credential; agents must not request either from the human.
31
+ - Unknown and never-signed-in accounts must not yield a claimable request, but
32
+ the public response must not disclose account existence.
33
+ - The approval code must be printed in a copyable, server-supplied verification
34
+ URL. Opening it must not claim the request; the matching signed-in user must
35
+ explicitly review that exact code before approval controls appear.
36
+ - Only one claimed pending or approved-but-uncollected request may be active per
37
+ user. Unclaimed issued codes must not occupy that slot.
29
38
  - Interactive provisioning should launch the browser to the approval page when
30
39
  possible. `--open` should force launch; `--no-open` should suppress it.
31
40
  - Cached developer tokens must be mode `0600`, gitignored, and reused until
32
41
  expiry.
42
+ - Minted developer tokens must have a metadata-only inventory (owner, label,
43
+ scopes, created/expires/last-used/revoked state), immediate owner revocation,
44
+ and global admin incident response. Plaintext is shown only once and never
45
+ returned by inventory.
46
+ - Handshake attempts must retain bounded metadata-only attribution using a
47
+ keyed requester fingerprint rather than a raw IP, with independent issue and
48
+ poll rate limits. Agents and developer tokens must not read this admin trail.
33
49
 
34
50
  ## Safe Provisioning
35
51
 
package/dist/bin.cjs CHANGED
@@ -203,14 +203,16 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
203
203
  return cached.token;
204
204
  }
205
205
  const browser = approvalBrowser(options);
206
+ const email = handshakeEmail(options.email, cached?.platform === audience ? cached.email : void 0);
206
207
  const { token, expiresAt } = await (0, import_db.requestToken)({
207
208
  endpoint: cfg.platformUrl,
209
+ email,
208
210
  label: `${cfg.app.id} provisioner`,
209
211
  fetch: doFetch,
210
- onCode: async ({ userCode, expiresIn }) => {
211
- const approvalUrl = handshakeUrl(cfg.platformUrl, userCode);
212
+ onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
213
+ const approvalUrl = verificationUriComplete ?? handshakeUrl(cfg.platformUrl, userCode);
212
214
  out.log("");
213
- out.log(`Approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
215
+ out.log(`Review, then approve code ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
214
216
  if (browser.open) {
215
217
  try {
216
218
  await (options.openApprovalUrl ?? openUrl)(approvalUrl);
@@ -224,10 +226,17 @@ async function getDeveloperToken(cfg, options, doFetch, out) {
224
226
  out.log("");
225
227
  }
226
228
  });
227
- writePrivateJson(cfg.local.tokenFile, { platform: audience, token, expiresAt });
229
+ writePrivateJson(cfg.local.tokenFile, { platform: audience, email, token, expiresAt });
228
230
  out.log(`auth: developer token cached (${displayPath(cfg.local.tokenFile, cfg.rootDir)})`);
229
231
  return token;
230
232
  }
233
+ function handshakeEmail(value, cached) {
234
+ const email = (value ?? import_node_process2.default.env.ODLA_USER_EMAIL ?? cached ?? "").trim().toLowerCase();
235
+ if (email.length > 254 || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) {
236
+ throw new Error("a fresh odla handshake requires --email <account> or ODLA_USER_EMAIL");
237
+ }
238
+ return email;
239
+ }
231
240
  function approvalBrowser(options) {
232
241
  if (options.open === true) return { open: true, mode: "forced" };
233
242
  if (options.open === false) return { open: false, reason: "disabled by --no-open" };
@@ -320,14 +329,16 @@ async function scopedToken(platform, scope, options, doFetch, out) {
320
329
  out.log(`auth: using cached ${scope} grant (${tokenFile})`);
321
330
  return cached.token;
322
331
  }
332
+ const email = handshakeEmail(options.email, cache?.platform === audience ? cache.email : void 0);
323
333
  const { token, expiresAt } = await (0, import_db2.requestToken)({
324
334
  endpoint: audience,
335
+ email,
325
336
  label: `odla CLI admin AI (${scope})`,
326
337
  scopes: [scope],
327
338
  fetch: doFetch,
328
- onCode: async ({ userCode, expiresIn }) => {
329
- const approvalUrl = handshakeUrl(audience, userCode);
330
- out.log(`Approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
339
+ onCode: async ({ userCode, expiresIn, verificationUriComplete }) => {
340
+ const approvalUrl = verificationUriComplete ?? handshakeUrl(audience, userCode);
341
+ out.log(`Review, then approve scoped request ${userCode} at ${approvalUrl} within ${Math.floor(expiresIn / 60)}m.`);
331
342
  const shouldOpen = options.open === true || options.open !== false && !import_node_process4.default.env.CI && Boolean(import_node_process4.default.stdin.isTTY && import_node_process4.default.stdout.isTTY);
332
343
  if (shouldOpen) await (options.openApprovalUrl ?? openUrl)(approvalUrl).catch(() => void 0);
333
344
  }
@@ -335,7 +346,7 @@ async function scopedToken(platform, scope, options, doFetch, out) {
335
346
  const tokens = cache?.platform === audience ? { ...cache.tokens ?? {} } : {};
336
347
  tokens[scope] = { token, expiresAt };
337
348
  if ((0, import_node_fs2.existsSync)(`${import_node_process4.default.cwd()}/.git`)) ensureGitignore(import_node_process4.default.cwd(), [tokenFile]);
338
- writePrivateJson(tokenFile, { platform: audience, tokens });
349
+ writePrivateJson(tokenFile, { platform: audience, email, tokens });
339
350
  out.log(`auth: cached ${scope} grant (${tokenFile}; mode 0600)`);
340
351
  return token;
341
352
  }
@@ -505,6 +516,7 @@ async function adminAi(options) {
505
516
  platform,
506
517
  scope,
507
518
  token: options.token,
519
+ email: options.email,
508
520
  open: options.open,
509
521
  fetch: doFetch,
510
522
  stdout: out,
@@ -764,6 +776,7 @@ async function adminCommand(parsed) {
764
776
  "platform",
765
777
  "json",
766
778
  "open",
779
+ "email",
767
780
  "provider",
768
781
  "model",
769
782
  "enabled",
@@ -791,6 +804,7 @@ async function adminCommand(parsed) {
791
804
  maxOutputTokens: numberOpt(parsed.options["max-output-tokens"], "--max-output-tokens"),
792
805
  maxCallsPerRun: numberOpt(parsed.options["max-calls-per-run"], "--max-calls-per-run"),
793
806
  platform: stringOpt(parsed.options.platform),
807
+ email: stringOpt(parsed.options.email),
794
808
  json: parsed.options.json === true,
795
809
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
796
810
  credentialProvider: credentialSet ? parsed.positionals[4] : void 0,
@@ -810,6 +824,7 @@ async function adminCommand(parsed) {
810
824
  // src/capabilities.ts
811
825
  var CAPABILITIES = {
812
826
  cli: [
827
+ "start an email-bound device request without accepting a password or user session token",
813
828
  "register the app and enable configured services per environment",
814
829
  "issue, persist, and explicitly rotate configured service credentials",
815
830
  "write local Worker values and push deployed Worker secrets through Wrangler stdin",
@@ -828,7 +843,7 @@ var CAPABILITIES = {
828
843
  "wire @odla-ai/calendar into trusted Worker code and keep the app admin key out of browsers"
829
844
  ],
830
845
  human: [
831
- "approve the odla device code and one-off third-party logins",
846
+ "provide the existing odla account email, then sign in and explicitly review/approve the exact device code",
832
847
  "review mirrored calendar/attendee disclosure and complete the server-issued Google consent flow",
833
848
  "consent to production changes with --yes",
834
849
  "request destructive credential rotation explicitly",
@@ -838,6 +853,7 @@ var CAPABILITIES = {
838
853
  ],
839
854
  studio: [
840
855
  "view telemetry and environment state",
856
+ "let signed-in users inventory/revoke their own agent grants and admins audit/global-revoke them",
841
857
  "review calendar connection, granted read scope, selected calendars, and sync health without exposing provider tokens",
842
858
  "perform manual credential recovery when the CLI's local shown-once copy is unavailable",
843
859
  "configure system AI purposes and view app/environment/run-attributed usage",
@@ -1371,6 +1387,7 @@ async function lifecycleContext(options) {
1371
1387
  {
1372
1388
  configPath: cfg.configPath,
1373
1389
  token: options.token,
1390
+ email: options.email,
1374
1391
  open: options.open,
1375
1392
  interactive: options.interactive,
1376
1393
  openApprovalUrl: options.openConsentUrl
@@ -1787,13 +1804,13 @@ Usage:
1787
1804
  odla-ai setup [--dir <project>] [--agent <name>] [--global] [--force]
1788
1805
  odla-ai init --app-id <id> --name <name> [--services db,ai,o11y,calendar] [--env dev --env prod]
1789
1806
  odla-ai doctor [--config odla.config.mjs]
1790
- odla-ai calendar status [--env dev] [--json]
1791
- odla-ai calendar calendars [--env dev] [--json]
1792
- odla-ai calendar connect [--env dev] [--no-open] [--yes]
1793
- odla-ai calendar resync [--env dev] [--yes]
1794
- odla-ai calendar disconnect [--env dev] --yes
1807
+ odla-ai calendar status [--env dev] [--email <odla-account>] [--json]
1808
+ odla-ai calendar calendars [--env dev] [--email <odla-account>] [--json]
1809
+ odla-ai calendar connect [--env dev] [--email <odla-account>] [--no-open] [--yes]
1810
+ odla-ai calendar resync [--env dev] [--email <odla-account>] [--yes]
1811
+ odla-ai calendar disconnect [--env dev] [--email <odla-account>] --yes
1795
1812
  odla-ai capabilities [--json]
1796
- odla-ai admin ai show [--platform https://odla.ai] [--json]
1813
+ odla-ai admin ai show [--platform https://odla.ai] [--email <odla-account>] [--json]
1797
1814
  odla-ai admin ai models [--provider <id>] [--json]
1798
1815
  odla-ai admin ai set <purpose> [--provider <id>] [--model <id>] [--enabled|--no-enabled]
1799
1816
  odla-ai admin ai set security --discovery-provider <id> --discovery-model <id>
@@ -1802,7 +1819,7 @@ Usage:
1802
1819
  odla-ai admin ai credential set <provider> (--from-env <NAME>|--stdin)
1803
1820
  odla-ai admin ai usage [--app-id <id>] [--env <env>] [--run-id <id>] [--limit <1-500>] [--json]
1804
1821
  odla-ai admin ai audit [--limit <1-200>] [--json]
1805
- odla-ai security github connect [--repo owner/name] [--env dev] [--no-open]
1822
+ odla-ai security github connect [--repo owner/name] [--env dev] [--email <odla-account>] [--no-open]
1806
1823
  odla-ai security github disconnect --source <id> [--env dev] [--yes]
1807
1824
  odla-ai security plan [--env dev] [--json]
1808
1825
  odla-ai security sources [--env dev] [--json]
@@ -1811,12 +1828,12 @@ Usage:
1811
1828
  odla-ai security report <job-id> [--json]
1812
1829
  odla-ai security run [target] --ack-redacted-source [--env dev] [--profile odla] [--fail-on high]
1813
1830
  odla-ai security run [target] --self --ack-redacted-source
1814
- odla-ai provision [--config odla.config.mjs] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1815
- odla-ai smoke [--config odla.config.mjs] [--env dev] [--no-open]
1831
+ odla-ai provision [--config odla.config.mjs] [--email <odla-account>] [--dry-run] [--push-secrets] [--rotate-o11y-token] [--write-dev-vars[=path]] [--yes]
1832
+ odla-ai smoke [--config odla.config.mjs] [--env dev] [--email <odla-account>] [--no-open]
1816
1833
  odla-ai skill install [--dir <project>] [--agent <name>] [--global] [--force]
1817
1834
  odla-ai secrets push --env <env> [--config odla.config.mjs] [--dry-run] [--yes]
1818
- odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
1819
- odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--config odla.config.mjs] [--yes]
1835
+ odla-ai secrets set <name> --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
1836
+ odla-ai secrets set-clerk-key --env <env> (--from-env <NAME>|--stdin) [--email <odla-account>] [--config odla.config.mjs] [--yes]
1820
1837
  odla-ai version
1821
1838
 
1822
1839
  Commands:
@@ -1844,6 +1861,10 @@ Safety:
1844
1861
  preflights Wrangler before any shown-once issuance or destructive rotation.
1845
1862
  Provision opens the approval page automatically in interactive terminals;
1846
1863
  use --open to force browser launch or --no-open to suppress it.
1864
+ A fresh device handshake requires --email <odla-account> or ODLA_USER_EMAIL.
1865
+ The email is a non-secret identity hint: never provide a password or session
1866
+ token. The matching account must already exist, be signed in, explicitly
1867
+ review the exact code, and finish any current request before claiming another.
1847
1868
  Calendar setup has a second human checkpoint: odla issues a state-bound Google consent URL;
1848
1869
  OAuth codes and refresh tokens never enter the CLI, repo, chat, or app.
1849
1870
  GitHub security uses source-read-only access plus optional metadata-only Checks write: the CLI never asks
@@ -2474,7 +2495,7 @@ async function hostedSecurityContext(parsed, dependencies) {
2474
2495
  const open = parsed.options.open === false ? false : parsed.options.open === true ? true : void 0;
2475
2496
  const token = await getDeveloperToken(
2476
2497
  cfg,
2477
- { configPath, open, openApprovalUrl: dependencies.openUrl },
2498
+ { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
2478
2499
  doFetch,
2479
2500
  stdout
2480
2501
  );
@@ -3092,6 +3113,7 @@ async function runSourceSecurityCommand(parsed, dependencies, sourceId) {
3092
3113
  "source",
3093
3114
  "ref",
3094
3115
  "follow",
3116
+ "email",
3095
3117
  "open",
3096
3118
  "json",
3097
3119
  "ack-redacted-source",
@@ -3180,6 +3202,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
3180
3202
  "platform",
3181
3203
  "self",
3182
3204
  "ack-redacted-source",
3205
+ "email",
3183
3206
  "open",
3184
3207
  "fail-on",
3185
3208
  "fail-on-candidates",
@@ -3209,6 +3232,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
3209
3232
  return getScopedPlatformToken({
3210
3233
  platform: request.platform,
3211
3234
  scope: request.scope,
3235
+ email: stringOpt(parsed.options.email),
3212
3236
  open,
3213
3237
  fetch: doFetch,
3214
3238
  stdout: out,
@@ -3221,7 +3245,7 @@ async function runLocalSecurityCommand(parsed, dependencies) {
3221
3245
  }
3222
3246
  return getDeveloperToken(
3223
3247
  cfg,
3224
- { configPath, open, openApprovalUrl: dependencies.openUrl },
3248
+ { configPath, email: stringOpt(parsed.options.email), open, openApprovalUrl: dependencies.openUrl },
3225
3249
  doFetch,
3226
3250
  out
3227
3251
  );
@@ -3255,7 +3279,7 @@ async function securityCommand(parsed, dependencies) {
3255
3279
  return;
3256
3280
  }
3257
3281
  if (sub === "plan") {
3258
- assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
3282
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
3259
3283
  const context = await hostedSecurityContext(parsed, dependencies);
3260
3284
  const plan = await getHostedSecurityPlan(context);
3261
3285
  if (parsed.options.json === true) context.stdout.log(JSON.stringify(plan, null, 2));
@@ -3271,7 +3295,7 @@ async function securityCommand(parsed, dependencies) {
3271
3295
  return;
3272
3296
  }
3273
3297
  if (sub === "report") {
3274
- assertArgs(parsed, ["config", "env", "platform", "open", "json"], 3);
3298
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 3);
3275
3299
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
3276
3300
  const context = await hostedSecurityContext(parsed, dependencies);
3277
3301
  const report = await getHostedSecurityReport({ ...context, jobId });
@@ -3289,7 +3313,7 @@ async function securityCommand(parsed, dependencies) {
3289
3313
  async function githubSecurityCommand(parsed, dependencies) {
3290
3314
  const action = parsed.positionals[2];
3291
3315
  if (action === "disconnect") {
3292
- assertArgs(parsed, ["config", "env", "platform", "source", "open", "yes"], 3);
3316
+ assertArgs(parsed, ["config", "env", "platform", "source", "email", "open", "yes"], 3);
3293
3317
  const context2 = await hostedSecurityContext(parsed, dependencies);
3294
3318
  const sourceId = requiredString(parsed.options.source, "--source");
3295
3319
  const confirmed = parsed.options.yes === true || await interactiveConfirmation(
@@ -3306,7 +3330,7 @@ async function githubSecurityCommand(parsed, dependencies) {
3306
3330
  if (action !== "connect") {
3307
3331
  throw new Error('unknown security github command. Try "odla-ai security github connect".');
3308
3332
  }
3309
- assertArgs(parsed, ["config", "env", "platform", "repo", "open"], 3);
3333
+ assertArgs(parsed, ["config", "env", "platform", "repo", "email", "open"], 3);
3310
3334
  const context = await hostedSecurityContext(parsed, dependencies);
3311
3335
  const repository = stringOpt(parsed.options.repo) ?? await inferGitHubRepository(process.cwd(), dependencies.readGitOrigin);
3312
3336
  const connection = await connectGitHubSecuritySource({
@@ -3321,7 +3345,7 @@ async function githubSecurityCommand(parsed, dependencies) {
3321
3345
  context.stdout.log("github: odla.ai stores the installation; no PAT or GitHub token is written locally");
3322
3346
  }
3323
3347
  async function listSecuritySources(parsed, dependencies) {
3324
- assertArgs(parsed, ["config", "env", "platform", "open", "json"], 2);
3348
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json"], 2);
3325
3349
  const context = await hostedSecurityContext(parsed, dependencies);
3326
3350
  const [plan, sources] = await Promise.all([
3327
3351
  getHostedSecurityPlan(context),
@@ -3343,7 +3367,7 @@ source repository default ref status`);
3343
3367
  }
3344
3368
  }
3345
3369
  async function securityStatus(parsed, dependencies) {
3346
- assertArgs(parsed, ["config", "env", "platform", "open", "json", "follow"], 3);
3370
+ assertArgs(parsed, ["config", "env", "platform", "email", "open", "json", "follow"], 3);
3347
3371
  const jobId = requiredSecurityPositional(parsed, 2, "job id");
3348
3372
  const context = await hostedSecurityContext(parsed, dependencies);
3349
3373
  const job = parsed.options.follow === true ? await followHostedSecurityJob({
@@ -3646,6 +3670,7 @@ async function smoke(options) {
3646
3670
  {
3647
3671
  configPath: cfg.configPath,
3648
3672
  token: options.token,
3673
+ email: options.email,
3649
3674
  open: options.open,
3650
3675
  interactive: options.interactive,
3651
3676
  openApprovalUrl: options.openApprovalUrl
@@ -3793,11 +3818,12 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3793
3818
  return;
3794
3819
  }
3795
3820
  if (command === "smoke") {
3796
- assertArgs(parsed, ["config", "env", "token", "open"], 1);
3821
+ assertArgs(parsed, ["config", "env", "token", "email", "open"], 1);
3797
3822
  await smoke({
3798
3823
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3799
3824
  env: stringOpt(parsed.options.env),
3800
3825
  token: stringOpt(parsed.options.token),
3826
+ email: stringOpt(parsed.options.email),
3801
3827
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3802
3828
  openApprovalUrl: dependencies.openUrl,
3803
3829
  fetch: dependencies.fetch,
@@ -3835,7 +3861,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3835
3861
  if (command === "secrets") {
3836
3862
  const sub = parsed.positionals[1];
3837
3863
  if (sub === "set" || sub === "set-clerk-key") {
3838
- assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "yes"], sub === "set" ? 3 : 2);
3864
+ assertArgs(parsed, ["config", "env", "from-env", "stdin", "token", "email", "yes"], sub === "set" ? 3 : 2);
3839
3865
  const options = {
3840
3866
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3841
3867
  name: parsed.positionals[2],
@@ -3843,6 +3869,7 @@ async function runCli(argv = process.argv.slice(2), dependencies = {}) {
3843
3869
  fromEnv: stringOpt(parsed.options["from-env"]),
3844
3870
  stdin: parsed.options.stdin === true,
3845
3871
  token: stringOpt(parsed.options.token),
3872
+ email: stringOpt(parsed.options.email),
3846
3873
  yes: parsed.options.yes === true,
3847
3874
  fetch: dependencies.fetch,
3848
3875
  stdout: dependencies.stdout
@@ -3876,6 +3903,7 @@ async function provisionCommand(parsed, dependencies) {
3876
3903
  "write-credentials",
3877
3904
  "write-dev-vars",
3878
3905
  "token",
3906
+ "email",
3879
3907
  "open",
3880
3908
  "yes"
3881
3909
  ], 1);
@@ -3889,6 +3917,7 @@ async function provisionCommand(parsed, dependencies) {
3889
3917
  writeCredentials: parsed.options["write-credentials"] !== false,
3890
3918
  writeDevVars: typeof writeDevVars2 === "string" ? writeDevVars2 : writeDevVars2 === true,
3891
3919
  token: stringOpt(parsed.options.token),
3920
+ email: stringOpt(parsed.options.email),
3892
3921
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3893
3922
  yes: parsed.options.yes === true,
3894
3923
  fetch: dependencies.fetch,
@@ -3902,13 +3931,14 @@ async function calendarCommand(parsed, dependencies) {
3902
3931
  if (sub !== "status" && sub !== "calendars" && sub !== "connect" && sub !== "resync" && sub !== "disconnect") {
3903
3932
  throw new Error(`unknown calendar subcommand "${sub ?? ""}". Try "odla-ai calendar status --env dev".`);
3904
3933
  }
3905
- assertArgs(parsed, ["config", "env", "json", "token", "open", "yes"], 2);
3934
+ assertArgs(parsed, ["config", "env", "json", "token", "email", "open", "yes"], 2);
3906
3935
  if (sub !== "status" && sub !== "calendars" && parsed.options.json !== void 0) throw new Error(`--json is supported only by calendar status/calendars`);
3907
3936
  if ((sub === "status" || sub === "calendars") && parsed.options.yes !== void 0) throw new Error(`--yes is not used by "calendar ${sub}"`);
3908
3937
  const options = {
3909
3938
  configPath: stringOpt(parsed.options.config) ?? "odla.config.mjs",
3910
3939
  env: stringOpt(parsed.options.env),
3911
3940
  token: stringOpt(parsed.options.token),
3941
+ email: stringOpt(parsed.options.email),
3912
3942
  open: parsed.options.open === false ? false : parsed.options.open === true ? true : void 0,
3913
3943
  yes: parsed.options.yes === true,
3914
3944
  json: parsed.options.json === true,